diff --git a/CHANGELOG.md b/CHANGELOG.md index 043f9c21..2b5c90a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Input line length and angle using textboxes +- Português (Brasil), Suomi, and 简体中文 translations +- Opened file name shows in the stitch menu +- Screenshot testing + +### Fixed + +- Menus will not hide when inputting text +- Able to drag ends of lines on touch devices +- Pressing C resets & recenters the pattern +- App crashing from large width/height inputs by setting the limit to 1000 +- Bright instead of pale green lines for SVGs when the green theme is enabled + +### Changed + +- Line measurement labels match the theme +- Enabled measuring when magnified for precision measuring +- Pressing an arrow button once will move the pattern by 1/8" or 2mm +- Slow down speed of pattern movement with arrow keys + ## [1.3.0] - 2025-06-25 ### Added diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index cb18ef20..4dca18c0 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -30,7 +30,7 @@ import { themeFilter, Theme, } from "@/_lib/display-settings"; -import { getPtDensity, IN } from "@/_lib/unit"; +import { getPtDensity, Unit } from "@/_lib/unit"; import { visible } from "@/_components/theme/css-functions"; import { useTranslations } from "next-intl"; import MeasureCanvas from "@/_components/canvases/measure-canvas"; @@ -87,6 +87,7 @@ export default function Page() { // Default dimensions should be available on most cutting mats and large enough to get an accurate calibration const defaultWidthDimensionValue = "24"; const defaultHeightDimensionValue = "16"; + const maxDimensionValue = 1000; // Prevents crashing from excessive grid lines #410 const maxPoints = 4; // One point per vertex in rectangle @@ -115,7 +116,7 @@ export default function Page() { const [restoreTransforms, setRestoreTransforms] = useState(null); const [pageCount, setPageCount] = useState(0); - const [unitOfMeasure, setUnitOfMeasure] = useState(IN); + const [unitOfMeasure, setUnitOfMeasure] = useState(Unit.IN); const [layoutWidth, setLayoutWidth] = useState(0); const [layoutHeight, setLayoutHeight] = useState(0); const [lineThickness, setLineThickness] = useState(0); @@ -267,14 +268,14 @@ export default function Page() { // Save valid calibration grid height in localStorage function handleHeightChange(e: ChangeEvent) { - const h = removeNonDigits(e.target.value, heightInput); + const h = removeNonDigits(e.target.value, heightInput, maxDimensionValue); setHeightInput(h); updateLocalSettings({ height: h }); } // Save valid calibration grid width in localStorage function handleWidthChange(e: ChangeEvent) { - const w = removeNonDigits(e.target.value, widthInput); + const w = removeNonDigits(e.target.value, widthInput, maxDimensionValue); setWidthInput(w); updateLocalSettings({ width: w }); } @@ -521,6 +522,7 @@ export default function Page() {
@@ -530,11 +532,11 @@ export default function Page() { className="bg-white dark:bg-black transition-all duration-500 w-screen h-screen" > {showCalibrationAlert ? ( -
+

{t("calibrationAlert")}

); } -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}`; -} diff --git a/app/_components/canvases/overlay-canvas.tsx b/app/_components/canvases/overlay-canvas.tsx index ddc3f341..187b03b4 100644 --- a/app/_components/canvases/overlay-canvas.tsx +++ b/app/_components/canvases/overlay-canvas.tsx @@ -10,6 +10,7 @@ import { import { useTransformContext } from "@/_hooks/use-transform-context"; import Matrix from "ml-matrix"; import { useTranslations } from "next-intl"; +import { Unit } from "@/_lib/unit"; export default function OverlayCanvas({ className, @@ -28,7 +29,7 @@ export default function OverlayCanvas({ points: Point[]; width: number; height: number; - unitOfMeasure: string; + unitOfMeasure: Unit; displaySettings: DisplaySettings; calibrationTransform: Matrix; zoomedOut: boolean; diff --git a/app/_components/draggable.tsx b/app/_components/draggable.tsx index 9834fbb2..a74dc545 100644 --- a/app/_components/draggable.tsx +++ b/app/_components/draggable.tsx @@ -20,7 +20,7 @@ import { } from "@/_lib/geometry"; import { Point } from "@/_lib/point"; import { CSS_PIXELS_PER_INCH } from "@/_lib/pixels-per-inch"; -import { IN } from "@/_lib/unit"; +import { Unit } from "@/_lib/unit"; import useProgArrowKeyToMatrix from "@/_hooks/use-prog-arrow-key-to-matrix"; import { visible } from "./theme/css-functions"; import { @@ -54,7 +54,7 @@ export default function Draggable({ children: ReactNode; perspective: Matrix; isCalibrating: boolean; - unitOfMeasure: string; + unitOfMeasure: Unit; calibrationTransform: Matrix; setCalibrationTransform: Dispatch>; setPerspective: Dispatch>; @@ -77,12 +77,12 @@ export default function Draggable({ const transform = useTransformContext(); const transformer = useTransformerContext(); - const quarterInchPx = CSS_PIXELS_PER_INCH / 4; - const halfCmPx = CSS_PIXELS_PER_INCH / 2.54 / 2; + const eighthInchPx = CSS_PIXELS_PER_INCH / 8; + const twoMmPx = CSS_PIXELS_PER_INCH / 12.7; useProgArrowKeyToMatrix( !isCalibrating, - unitOfMeasure === IN ? quarterInchPx : halfCmPx, + unitOfMeasure === Unit.IN ? eighthInchPx : twoMmPx, (matrix) => { transformer.setLocalTransform(matrix.mmul(transform)); }, diff --git a/app/_components/header.tsx b/app/_components/header.tsx index 6bc67c5b..42056b09 100644 --- a/app/_components/header.tsx +++ b/app/_components/header.tsx @@ -31,7 +31,7 @@ import { strokeColor, themes, } from "@/_lib/display-settings"; -import { CM, IN } from "@/_lib/unit"; +import { Unit } from "@/_lib/unit"; import RecenterIcon from "@/_icons/recenter-icon"; import { getCalibrationCenterPoint } from "@/_lib/geometry"; import { visible } from "@/_components/theme/css-functions"; @@ -120,8 +120,8 @@ export default function Header({ handleFileChange: (e: ChangeEvent) => void; handleResetCalibration: () => void; fullScreenHandle: FullScreenHandle; - unitOfMeasure: string; - setUnitOfMeasure: (newUnit: string) => void; + unitOfMeasure: Unit; + setUnitOfMeasure: (newUnit: Unit) => void; displaySettings: DisplaySettings; setDisplaySettings: (newDisplaySettings: DisplaySettings) => void; layoutWidth: number; @@ -224,7 +224,8 @@ export default function Header({ ); }; - const handleRecenter = () => { + const handleRecenterReset = () => { + transformer.reset(); transformer.recenter( getCalibrationCenterPoint(width, height, unitOfMeasure), layoutWidth, @@ -324,7 +325,7 @@ export default function Header({ }, [KeyCode.KeyV]); useKeyDown(() => { - handleRecenter(); + handleRecenterReset(); }, [KeyCode.KeyC]); useKeyDown(() => { @@ -470,7 +471,7 @@ export default function Header({
setUnitOfMeasure(e.target.value)} + handleChange={(e) => setUnitOfMeasure(e.target.value as Unit)} id="unit_of_measure" name="unit_of_measure" value={unitOfMeasure} options={[ - { value: IN, label: "in" }, - { value: CM, label: "cm" }, + { value: Unit.IN, label: "in" }, + { value: Unit.CM, label: "cm" }, ]} />
@@ -557,14 +558,7 @@ export default function Header({ { - transformer.reset(); - transformer.recenter( - getCalibrationCenterPoint(width, height, unitOfMeasure), - layoutWidth, - layoutHeight, - ); - }} + onClick={handleRecenterReset} > @@ -591,7 +585,6 @@ export default function Header({ setMeasuring(!measuring)} active={measuring} - disabled={magnifying} > diff --git a/app/_components/inline-input.tsx b/app/_components/inline-input.tsx index 4fde665e..c0c43b11 100644 --- a/app/_components/inline-input.tsx +++ b/app/_components/inline-input.tsx @@ -21,6 +21,7 @@ export default function InlineInput({ value, min, type, + disabled = false, }: { className?: string | undefined; inputClassName?: string | undefined; @@ -33,6 +34,7 @@ export default function InlineInput({ value: string; type?: string; min?: string; + disabled?: boolean; }) { return (
@@ -43,9 +45,10 @@ export default function InlineInput({ {label} >; lines: Line[]; - setLines: Dispatch>; handleDeleteLine: () => void; gridCenter: Point; setMeasuring: Dispatch>; menusHidden: boolean; menuStates: MenuStates; + unitOfMeasure: Unit; + dispatchLines: Dispatch; }) { const t = useTranslations("MeasureCanvas"); const transformer = useTransformerContext(); @@ -75,81 +84,140 @@ export default function LineMenu({ ); } - const grainLine: Line = [ + const grainLine = createLine( gridCenter, - { x: gridCenter.x + 1, y: gridCenter.y }, - ]; + { + x: gridCenter.x + 1, + y: gridCenter.y, + }, + unitOfMeasure, + ); return ( - // center menu items horizontally - = 0 && !menusHidden)}`} - > -
- {lines.length} - {lines.length === 1 ? t("line") : t("lines")} -
- - { - if (matLine) { - transformer.align(matLine, grainLine); - } - }} - /> - { - if (lines.length > 0) { - const previous = - selectedLine <= 0 ? lines.length - 1 : selectedLine - 1; - setSelectedLine(previous); - transformer.align(getMatLine(previous), grainLine); - } - }} - /> - { - if (lines.length > 0) { - const next = - selectedLine + 1 >= lines.length ? 0 : selectedLine + 1; - setSelectedLine(next); - transformer.align(getMatLine(next), grainLine); - } - }} - /> - { - if (matLine) { - transformer.flipAlong(matLine); - } - }} - /> - { - if (matLine) { - transformer.translate(subtract(matLine[1], matLine[0])); - if (selected) { - const newLines = lines.slice(); - newLines[selectedLine] = [selected[1], selected[0]]; - setLines(newLines); + selected && ( + = 0 && !menusHidden)}`} + > +
+ {lines.length} + {lines.length === 1 ? t("line") : t("lines")} +
+ + { + if (matLine) { + transformer.align(matLine, grainLine); + } + }} + /> + { + if (lines.length > 0) { + const previous = + selectedLine <= 0 ? lines.length - 1 : selectedLine - 1; + setSelectedLine(previous); + transformer.align(getMatLine(previous), grainLine); + } + }} + /> + { + if (lines.length > 0) { + const next = + selectedLine + 1 >= lines.length ? 0 : selectedLine + 1; + setSelectedLine(next); + transformer.align(getMatLine(next), grainLine); + } + }} + /> + { + if (matLine) { + transformer.flipAlong(matLine); + } + }} + /> + { + if (matLine) { + transformer.translate( + subtract(matLine.points[1], matLine.points[0]), + ); + if (selected) { + dispatchLines({ + type: "update-both-points", + index: selectedLine, + newP0: selected.points[1], + newP1: selected.points[0], + }); + } } - } - }} - /> -
+ }} + /> + { + const newDistance = removeNonDigits( + e.target.value, + selected.distance, + ); + dispatchLines({ + type: "update-distance", + index: selectedLine, + newDistance, + }); + }} + id="distance" + labelRight={unitOfMeasure.toLocaleLowerCase()} + name="distance" + value={selected.distance} + type="string" + /> + { + const inputValue = e.target.value; + let newAngle; + + if (inputValue === "") { + newAngle = ""; + } else { + const numValue = parseInt(inputValue); + if (!isNaN(numValue) && numValue >= 0 && numValue <= 360) { + newAngle = String(numValue); + } else { + return; + } + } + dispatchLines({ + type: "update-angle", + index: selectedLine, + newAngle, + }); + }} + id="angle" + labelRight="°" + name="angle" + value={selected.angle} + type="string" + /> +
+ ) ); } diff --git a/app/_components/menus/stitch-menu.tsx b/app/_components/menus/stitch-menu.tsx index eefd1bf9..374c3c79 100644 --- a/app/_components/menus/stitch-menu.tsx +++ b/app/_components/menus/stitch-menu.tsx @@ -50,6 +50,7 @@ export default function StitchMenu({ +

{file?.name}

void, ) { - const PIXEL_LIST = [1, 10, 20, 40]; + const PIXEL_LIST = [1, 2, 4]; function moveWithArrowKey(key: string, px: number) { let newOffset: Point = { x: 0, y: 0 }; const dist = px * scale; diff --git a/app/_hooks/use-transform-context.tsx b/app/_hooks/use-transform-context.tsx index c0f24de5..9c25b664 100644 --- a/app/_hooks/use-transform-context.tsx +++ b/app/_hooks/use-transform-context.tsx @@ -1,5 +1,5 @@ import debounce from "@/_lib/debounce"; -import { Line } from "@/_lib/interfaces/line"; +import { Line } from "@/_reducers/linesReducer"; import { Point } from "@/_lib/point"; import localTransformReducer, { LocalTransformAction, diff --git a/app/_icons/full-screen-icon.tsx b/app/_icons/full-screen-icon.tsx index 3d60db87..fe6c4a37 100644 --- a/app/_icons/full-screen-icon.tsx +++ b/app/_icons/full-screen-icon.tsx @@ -1,6 +1,13 @@ -export default function FullScreenIcon({ ariaLabel }: { ariaLabel: string }) { +export default function FullScreenIcon({ + ariaLabel, + className, +}: { + ariaLabel: string; + className?: string; +}) { return ( , public hoverCorners: Set, - public unitOfMeasure: string, + public unitOfMeasure: Unit, public errorFillPattern: CanvasFillStrokeStyles["fillStyle"], public displaySettings: DisplaySettings, public isFlipped: boolean, @@ -54,7 +54,10 @@ export enum OverlayMode { NONE, } -export function drawLine(ctx: CanvasRenderingContext2D, line: Line): void { +export function drawLine( + ctx: CanvasRenderingContext2D, + line: SimpleLine, +): void { ctx.save(); ctx.beginPath(); ctx.moveTo(line[0].x, line[0].y); @@ -73,7 +76,10 @@ export function drawCircle( ctx.stroke(); } -export function drawArrow(ctx: CanvasRenderingContext2D, line: Line): void { +export function drawArrow( + ctx: CanvasRenderingContext2D, + line: SimpleLine, +): void { const dx = line[1].x - line[0].x; const dy = line[1].y - line[0].y; const angle = Math.atan2(dy, dx); @@ -206,10 +212,10 @@ function drawViewportOutline(cs: CanvasState) { export function drawCenterLines(cs: CanvasState) { const { width, height, ctx, perspective } = cs; ctx.save(); - ctx.strokeStyle = "red"; + ctx.strokeStyle = "#FF4500"; function drawProjectedLine(p1: Point, p2: Point) { - const line = transformLine([p1, p2], perspective); + const line = transformSimpleLine([p1, p2], perspective); ctx.lineWidth = 2; drawLine(ctx, line); ctx.stroke(); @@ -232,7 +238,7 @@ export function drawPaperSheet(cs: CanvasState) { ctx.fillStyle = "white"; const [text, paperWidth, paperHeight] = - unitOfMeasure == CM ? ["A4", 29.7, 21] : ["11x8.5", 11, 8.5]; + unitOfMeasure == Unit.CM ? ["A4", 29.7, 21] : ["11x8.5", 11, 8.5]; const cornersP = transformPoints( translatePoints( @@ -301,7 +307,7 @@ export function drawGrid( if (i % majorLine === 0 || i === cs.width) { lineWidth = cs.majorLineWidth; } - const line = transformLine( + const line = transformSimpleLine( [ { x: i, y: -outset }, { x: i, y: cs.height + outset }, @@ -319,7 +325,7 @@ export function drawGrid( lineWidth = cs.majorLineWidth; } const y = cs.height - i; - const line = transformLine( + const line = transformSimpleLine( [ { x: -outset, y: y }, { x: cs.width + outset, y: y }, @@ -347,7 +353,7 @@ export function drawDimensionLabels( width: number, height: number, perspective: Matrix, - unitOfMeasure: string, + unitOfMeasure: Unit, ) { const fontSize = 48; const inset = 36; @@ -380,7 +386,7 @@ export function drawDimensionLabels( function drawFlippedPattern(cs: CanvasState) { const { ctx } = cs; ctx.save(); - ctx.fillStyle = "red"; + ctx.fillStyle = "#FF4500"; // draw a grid of dots const dotSize = 2; const spacing = 72; diff --git a/app/_lib/geometry.ts b/app/_lib/geometry.ts index 1878302f..95ae1005 100644 --- a/app/_lib/geometry.ts +++ b/app/_lib/geometry.ts @@ -43,8 +43,9 @@ import { Point, subtract } from "@/_lib/point"; import { AbstractMatrix, Matrix, solve } from "ml-matrix"; -import { getPtDensity } from "./unit"; -import { Line } from "./interfaces/line"; +import { Unit, getPtDensity } from "@/_lib/unit"; + +export type SimpleLine = [Point, Point]; /** Calculates a perspective transform from four pairs of the corresponding points. * @@ -167,7 +168,7 @@ function getDstVertices( export function getCalibrationCenterPoint( width: number, height: number, - unitOfMeasure: string, + unitOfMeasure: Unit, ): Point { return { x: width * getPtDensity(unitOfMeasure) * 0.5, @@ -195,8 +196,8 @@ export function getPerspectiveTransformFromPoints( } } -export function transformLine(line: Line, m: Matrix): Line { - return [transformPoint(line[0], m), transformPoint(line[1], m)]; +export function transformSimpleLine(line: SimpleLine, m: Matrix): SimpleLine { + return transformPoints(line, m) as SimpleLine; } export function transformPoints(points: Point[], m: Matrix): Point[] { @@ -238,7 +239,7 @@ export function rotate(angle: number): Matrix { ]); } -export function align(line: Line, to: Line): Matrix { +export function align(line: SimpleLine, to: SimpleLine): 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); @@ -268,21 +269,29 @@ export function flipHorizontal(origin: Point): Matrix { return transformAboutPoint(scale(-1, 1), origin); } -export function angleDeg(line: Line): number { +export function angleDeg(line: SimpleLine): number { return angle(line) * (180 / Math.PI); } -export function angle(line: Line): number { +export function angle(line: SimpleLine): 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 { + +export function distance(line: SimpleLine): number { + const [p1, p2] = line; + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + return Math.sqrt(dx * dx + dy * dy); +} + +export function rotateToHorizontal(line: SimpleLine): Matrix { return rotateMatrixDeg(-angleDeg(line), line[0]); } -export function flipAlong(line: Line): Matrix { +export function flipAlong(line: SimpleLine): Matrix { const angle = angleDeg(line); const a = rotateMatrixDeg(-angle, line[0]); const b = translate({ x: 0, y: -line[0].y }); @@ -329,11 +338,11 @@ export function minIndex(a: number[]): number { return min; } -export function distToLine(line: Line, p: Point): number { +export function distToLine(line: SimpleLine, p: Point): number { return Math.sqrt(sqrDistToLine(line, p)); } -export function sqrDistToLine(line: Line, p: Point): number { +export function sqrDistToLine(line: SimpleLine, p: Point): number { const [a, b] = line; const len2 = sqrDist(a, b); if (len2 === 0) { diff --git a/app/_lib/interfaces/line.ts b/app/_lib/interfaces/line.ts deleted file mode 100644 index e12937aa..00000000 --- a/app/_lib/interfaces/line.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Point } from "../point"; - -export type Line = [Point, Point]; diff --git a/app/_lib/remove-non-digits.ts b/app/_lib/remove-non-digits.ts index 68dc42a3..7f9748c7 100644 --- a/app/_lib/remove-non-digits.ts +++ b/app/_lib/remove-non-digits.ts @@ -1,13 +1,14 @@ export default function removeNonDigits( newString: string, oldString: string, + max?: number, ): string { const num = newString.replace(/[^.\d]/g, ""); const decimalCount = (num.match(/\./g) || []).length; if (num.localeCompare(".") === 0) { return "0."; } - if (decimalCount > 1) { + if (decimalCount > 1 || (max !== undefined && parseFloat(num) > max)) { return oldString; } else { return num; diff --git a/app/_lib/unit.ts b/app/_lib/unit.ts index d14898fd..6e676fb8 100644 --- a/app/_lib/unit.ts +++ b/app/_lib/unit.ts @@ -1,5 +1,8 @@ -export const { CM, IN } = { IN: "IN", CM: "CM" }; +export enum Unit { + CM = "CM", + IN = "IN", +} -export function getPtDensity(unitOfMeasure: string): number { - return unitOfMeasure === CM ? 96 / 2.54 : 96; +export function getPtDensity(unitOfMeasure: Unit): number { + return unitOfMeasure === Unit.CM ? 96 / 2.54 : 96; } diff --git a/app/_reducers/layersReducer.ts b/app/_reducers/layersReducer.ts index 838fd2ca..53b8807b 100644 --- a/app/_reducers/layersReducer.ts +++ b/app/_reducers/layersReducer.ts @@ -1,34 +1,11 @@ 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; + | { type: "set-layers"; layers: Layers } + | { type: "update-visibility"; visibleLayers: Set } + | { type: "toggle-layer"; key: string } + | { type: "hide-all" } + | { type: "show-all" }; export default function layersReducer( layers: Layers, diff --git a/app/_reducers/linesReducer.ts b/app/_reducers/linesReducer.ts new file mode 100644 index 00000000..1906884d --- /dev/null +++ b/app/_reducers/linesReducer.ts @@ -0,0 +1,223 @@ +import { Unit } from "@/_lib/unit"; +import { Point } from "@/_lib/point"; +import { + SimpleLine, + angleDeg, + dist, + transformSimpleLine, +} from "@/_lib/geometry"; +import { CSS_PIXELS_PER_INCH } from "@/_lib/pixels-per-inch"; +import Matrix from "ml-matrix"; + +export interface Line { + points: SimpleLine; + distance: string; // controlled input for distance + angle: string; // controlled input for angle + unitOfMeasure: Unit; // unit of measure for distance +} + +export function createLine( + p0: Point, + p1: Point, + unitOfMeasure = Unit.IN, +): Line { + const line: Line = { + points: [p0, p1], + distance: "0", + angle: "0", + unitOfMeasure, + }; + return updateLineMeasurements(line); +} + +export function transformLine(line: Line, m: Matrix): Line { + const newLine = { + ...line, + points: transformSimpleLine(line.points, m), + }; + return updateLineMeasurements(newLine); +} + +export function calculateDistance(line: Line): number { + let d = dist(...line.points) / CSS_PIXELS_PER_INCH; + if (line.unitOfMeasure === Unit.CM) { + d *= 2.54; + } + return d; +} + +// Recalculates the distance and angle based on the line's points. +export function updateLineMeasurements( + line: Line, + isConstrained = false, +): Line { + // Apply a fixed precision of 3 decimal places to the distance. + const distance = calculateDistance(line).toFixed(3); + + // To match a standard y-up system, we flip the y-axis for the angle calculation. + const dx = line.points[1].x - line.points[0].x; + const dy = line.points[1].y - line.points[0].y; + let a = (Math.atan2(-dy, dx) * 180) / Math.PI; + + if (a < 0) a += 360; + + let angle = a.toFixed(0); + + if (isConstrained) { + angle = (Math.round(a / 45) * 45).toFixed(0); + } + if (angle === "360") angle = "0"; + return { ...line, distance, angle }; +} + +// Calculates a point based on a starting point, distance, and angle. +// This function assumes CCW angle (0 right, 90 up) and corrects for screen's inverted y-axis. +function calculatePointFromMetrics( + startPoint: Point, + distance: number, + angle: number, +): Point { + const angleInRadians = (angle * Math.PI) / 180; + return { + x: startPoint.x + distance * Math.cos(angleInRadians), + // This ensures 90 degrees moves the point UP (Y-value decreases). + y: startPoint.y - distance * Math.sin(angleInRadians), + }; +} + +export type LinesAction = + | { type: "set"; lines: Line[] } + | { + type: "update-point"; + index: number; + pointIndex: 0 | 1; + newPoint: Point; + isConstrained: boolean; + } + | { type: "update-both-points"; index: number; newP0: Point; newP1: Point } + | { type: "update-distance"; index: number; newDistance: string } + | { type: "update-angle"; index: number; newAngle: string } + | { type: "add"; line: Line } + | { type: "remove"; index: number } + | { type: "reset" } + | { type: "update-unit-of-measure"; unitOfMeasure: Unit }; + +export default function linesReducer( + state: Line[], + action: LinesAction, +): Line[] { + switch (action.type) { + case "set": + return action.lines; + + case "update-point": + return state.map((line, i) => { + if (i === action.index) { + const newPoints = [...line.points] as SimpleLine; + newPoints[action.pointIndex] = action.newPoint; + // Recalculate distance and angle based on the new points + return updateLineMeasurements( + { ...line, points: newPoints }, + action.isConstrained, + ); + } + return line; + }); + + case "update-both-points": + return state.map((line, i) => { + if (i === action.index) { + const newPoints = [action.newP0, action.newP1] as SimpleLine; + // Recalculate distance and angle based on the new points + return updateLineMeasurements({ ...line, points: newPoints }); + } + return line; + }); + + case "update-distance": + return state.map((line, i) => { + if (i === action.index) { + const newDistanceString = action.newDistance; + const newDistanceNum = parseFloat(newDistanceString); + + // If the input is not a valid number, only update the distance string. + // This prevents the line's geometry from changing while the user is typing or backspacing. + if (isNaN(newDistanceNum)) { + return { ...line, distance: newDistanceString }; + } + + const distanceInPixels = + newDistanceNum * + CSS_PIXELS_PER_INCH * + (line.unitOfMeasure === Unit.CM ? 1 / 2.54 : 1); + + const p0 = line.points[0]; + // Use the precise angle from the line's points, not the rounded string. + const preciseAngle = angleDeg(line.points); + + const newP1 = calculatePointFromMetrics( + p0, + distanceInPixels, + preciseAngle, + ); + + const newPoints = [p0, newP1] as SimpleLine; + // Don't call updateLineMeasurements to avoid overwriting the user's input. + return { ...line, distance: newDistanceString, points: newPoints }; + } + return line; + }); + + case "update-angle": + return state.map((line, i) => { + if (i === action.index) { + const newAngleString = action.newAngle; + const newAngleNum = parseFloat(newAngleString); + + // If the input is not a valid number, only update the angle string. + if (isNaN(newAngleNum)) { + return { ...line, angle: newAngleString }; + } + + // Use the precise distance from the line's points, not the rounded string. + const preciseDistance = calculateDistance(line); + + const p0 = line.points[0]; + + const newP1 = calculatePointFromMetrics( + p0, + preciseDistance * CSS_PIXELS_PER_INCH, + newAngleNum, + ); + + const newPoints = [p0, newP1] as SimpleLine; + // Don't call updateLineMeasurements to avoid overwriting the user's input. + return { ...line, angle: newAngleString, points: newPoints }; + } + return line; + }); + + case "add": + return [...state, action.line]; + case "remove": + return state.filter((_, i) => i !== action.index); + case "reset": + return []; + case "update-unit-of-measure": + return state.map((line) => { + const newDistance = String( + calculateDistance({ + ...line, + unitOfMeasure: action.unitOfMeasure, + }), + ); + return { + ...line, + distance: newDistance, + unitOfMeasure: action.unitOfMeasure, + }; + }); + default: + return state; + } +} diff --git a/app/_reducers/localTransformReducer.ts b/app/_reducers/localTransformReducer.ts index 238b02e5..9f7cda52 100644 --- a/app/_reducers/localTransformReducer.ts +++ b/app/_reducers/localTransformReducer.ts @@ -10,74 +10,26 @@ import { translate, scaleAboutPoint, } from "@/_lib/geometry"; -import { Line } from "@/_lib/interfaces/line"; +import { Line } from "@/_reducers/linesReducer"; 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; + | { type: "flip_vertical" | "flip_horizontal"; centerPoint: Point } + | { type: "rotate"; centerPoint: Point; degrees: number } + | { + type: "recenter"; + centerPoint: Point; + layoutWidth: number; + layoutHeight: number; + } + | { type: "rotate_to_horizontal"; line: Line } + | { type: "flip_along"; line: Line } + | { type: "translate"; p: Point } + | { type: "set"; localTransform: Matrix } + | { type: "reset" } + | { type: "align"; line: Line; to: Line } + | { type: "magnify"; scale: number; point: Point }; export default function localTransformReducer( localTransform: Matrix, @@ -88,10 +40,10 @@ export default function localTransformReducer( return action.localTransform.clone(); } case "rotate_to_horizontal": { - return rotateToHorizontal(action.line).mmul(localTransform); + return rotateToHorizontal(action.line.points).mmul(localTransform); } case "flip_along": { - return flipAlong(action.line).mmul(localTransform); + return flipAlong(action.line.points).mmul(localTransform); } case "translate": { return translate(action.p).mmul(localTransform); @@ -116,7 +68,7 @@ export default function localTransformReducer( return Matrix.identity(3); } case "align": { - return align(action.line, action.to).mmul(localTransform); + return align(action.line.points, action.to.points).mmul(localTransform); } case "magnify": { return scaleAboutPoint(action.scale, action.point).mmul(localTransform); diff --git a/app/_reducers/patternScaleReducer.ts b/app/_reducers/patternScaleReducer.ts index 0f78efbe..121f75b5 100644 --- a/app/_reducers/patternScaleReducer.ts +++ b/app/_reducers/patternScaleReducer.ts @@ -1,14 +1,6 @@ -interface DeltaAction { - type: "delta"; - delta: number; -} - -interface SetAction { - type: "set"; - scale: string; -} - -export type PatternScaleAction = DeltaAction | SetAction; +export type PatternScaleAction = + | { type: "delta"; delta: number } + | { type: "set"; scale: string }; export default function PatternScaleReducer( patternScale: string, diff --git a/app/_reducers/pointsReducer.ts b/app/_reducers/pointsReducer.ts index f3cba018..438b8c73 100644 --- a/app/_reducers/pointsReducer.ts +++ b/app/_reducers/pointsReducer.ts @@ -1,17 +1,8 @@ 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 type PointAction = + | { type: "offset"; offset: Point; corners: Set } + | { type: "set"; points: Point[] }; export default function pointsReducer( points: Point[], diff --git a/app/_reducers/stitchSettingsReducer.ts b/app/_reducers/stitchSettingsReducer.ts index 07032e31..d8aff6f7 100644 --- a/app/_reducers/stitchSettingsReducer.ts +++ b/app/_reducers/stitchSettingsReducer.ts @@ -1,51 +1,14 @@ 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; + | { type: "set"; stitchSettings: StitchSettings } + | { type: "set-page-range"; pageRange: string } + | { type: "set-line-count"; lineCount: number; pageCount: number } + | { type: "step-line-count"; pageCount: number; step: number } + | { type: "step-horizontal"; step: number } + | { type: "step-vertical"; step: number } + | { type: "set-edge-insets"; edgeInsets: EdgeInsets }; export default function stitchSettingsReducer( stitchSettings: StitchSettings, diff --git a/app/manifest.js b/app/manifest.js index e9961d58..cac5e991 100644 --- a/app/manifest.js +++ b/app/manifest.js @@ -3,6 +3,7 @@ export default function manifest() { name: "Pattern Projector", short_name: "PatternProjector", start_url: "/calibrate", + id: "calibrate", icons: [ { src: "144.png", @@ -18,15 +19,28 @@ export default function manifest() { }, ], display: "fullscreen", + display_override: ["fullscreen", "minimal-ui", "standalone"], + orientation: "landscape", + lang: "en-US", + dir: "ltr", + scope: "https://patternprojector.com", + scope_extensions: [{ origin: "*.patternprojector.com" }], + prefer_related_applications: false, + launch_handler: { + client_mode: "navigate-existing", + }, + handle_links: "preferred", background_color: "#fff", description: "Calibrates projectors for projecting sewing patterns with accurate scaling and without perspective distortion", + categories: ["productivity", "design", "sewing"], theme_color: "#fff", file_handlers: [ { action: "/calibrate", accept: { "application/pdf": [".pdf"], + "image/svg+xml": [".svg"], }, }, ], diff --git a/messages/da.json b/messages/da.json index 7a6a0046..f1a86600 100644 --- a/messages/da.json +++ b/messages/da.json @@ -35,8 +35,8 @@ "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.", + "title": "Vis et mønster", + "open": "Klik eller tap for at åbne et mønster.", "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.", @@ -91,6 +91,18 @@ "showMenu": { "title": "Vis/skjul menu", "description": "Vis eller skjul topmenu." + }, + "magnify": { + "title": "Forstør (M)", + "description": "Klik eller tap på PDF'en for at forstørre den. Klik eller tap igen for at stoppe med at forstørre." + }, + "zoomOut": { + "title": "Zoom ud (Z)", + "description": "Zoom ud for at se hele PDF'en. Klik eller tap på PDF'en for at zoome ind igen på den valgte placering." + }, + "scale": { + "title": "Vis eller skjul skaleringsmenuen", + "description": "Ændr størrelse på mønsteret ved at indtaste en gangefaktor: Mellem 0 og 1 vil gøre mønsteret mindre og over 1 vil gøre mønsteret større." } }, "faq": { @@ -113,7 +125,7 @@ }, "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." + "answer": "Det er muligt at bruge hjemmesiden på en smartphone, men den begrænsede skærmstørrelse gør det svært at bruge." } }, "resources": { @@ -131,7 +143,7 @@ }, "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!", + "donation": "Hvis Pattern Projector at sparet dig penge i printshoppen eller på abonnement på en PDF-viser, så overvej at købe mig en kop kaffe eller støtte via PayPal!", "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!" @@ -151,11 +163,11 @@ "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." + "use": "For at folde mønsterdele ud: Tegn en linje på til fold-linjen af en mønsterdel, skær mønsterdelen ud frem til fold-linjen, klik på \"vend langs streg\"-knappen og skær resten af mønsterdelen ud." }, "move": { "title": "Forskyd PDF'en med linjens længde", - "description": "Forskyd PDF'en med linjens længde", + "description": "Forskyder 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": { @@ -194,7 +206,7 @@ "Header": { "projecting": "Visning af mønster", "calibrating": "Kalibrering", - "openPDF": "Åbn PDF", + "openPDF": "Åbn", "height": "H:", "width": "B:", "project": "Vis mønster", @@ -251,17 +263,21 @@ "flipCenterOff": "Vend hen over midten slået fra", "magnify": "Forstør (M)", "zoomOut": "Zoom ud (Z)", - "stitchMenuDisabled": "Åbn PDF for at bruge samlemenu" + "stitchMenuDisabled": "Åbn PDF for at bruge samlemenu", + "mail": "Mail fra Courtney", + "invalidCalibration": "Kalibreringsgitteret er ikke korrekt. Tryk på \"Nulstil kalibrering\" for at prøve igen." }, "StitchMenu": { "columnCount": "Kolonner", "pageRange": "Saml sider", - "horizontal": "Vandret", - "vertical": "Lodret" + "horizontal": "Vandret overlap", + "vertical": "Lodret overlap", + "rowCount": "Rækker", + "zeros": "Indsæt 0 for at indsætte en blank side, f.eks. 1-3,0,0,4" }, "MeasureCanvas": { "rotateToHorizontal": "Rotér til vandret", - "translate": "Forskyd PDF med linjens længde", + "translate": "Forskyd mønster med linjens længde", "deleteLine": "Slet linje", "flipAlong": "Vend langs linjen", "rotateAndCenterPrevious": "Bring forrige linje til center", @@ -272,9 +288,9 @@ "LayerMenu": { "layersOn": "Vis lagmenu", "layersOff": "Skjul lagmenu", - "noLayers": "Ingen lag i PDF", + "noLayers": "Ingen lag i mønsteret", "showAll": "Vis alle", - "hideAll": "Skjul all", + "hideAll": "Slå alle fra", "title": "Lag" }, "MovementPad": { @@ -287,17 +303,61 @@ "InstallButton": { "ok": "OK", "title": "Installér app", - "description": "For bedste brugeroplevelse, installér Pattern Projector på Google Chrome.", + "description": "For bedste brugeroplevelse, installér Pattern Projector på Google Chrome. I Chrome kan du downloade app'en ved at trykke på in adresselinjen.", "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" + "zoomedOut": "klik på mønsteret for at zoome ind", + "magnifying": "klik på mønsteret for at stoppe med forstørre", + "scaled": "× skala" }, "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." + }, + "Mail": { + "title": "Du har modtaget mail", + "donate": "Donér" + }, + "Troubleshooting": { + "notMatching": "Passer linjerne ikke på din måtte?", + "title": "Fejlsøg kalibrering", + "dragCorners": { + "title": "Hvor kalibrerer du", + "description": "Træk hjørnerne af gitteret ud til et rektangel på mindst 60x40 cm på måtten. Gitteret skal være så stort som muligt, men det behøver ikke at dække hele måtten.", + "caption": "Et korrekt kalibreret gitter." + }, + "inputMeasurement": "Brug et målebånd til at måle bredde og højde på gitteret på din måtte. Indtast disse mål i felterne til bredde og højde.", + "offByOne": { + "title": "Linjerne er lige men passer ikke?", + "description": "Hvis de projicerede linjer (linjerne på billedet fra projektoren) er lige men ikke passer med linjerne på måtten, er bredde og højde måske en 1 tomme/centimeter forkert. Dobbelttjek målene med et målebånd. Målene på måtten kan være forvirrende eller upræcise, så det er vigtigt at tjekke med et målebånd.", + "caption": "De indtastede mål er forkerte, der står 23x17 i stedet for 24x18. Måtten viser kun tallene op til 23 og 17, så det er nemt at tage fejl af målene. Husk at måle med et målebånd!" + }, + "unevenSurface": { + "title": "Buede linjer?", + "description": "Hvis de projicerede linjer (linjerne på billedet fra projektoren) ser buede ud, er din måtte eller underlaget under måtten måske ikke helt lige. Prøv at lægge karton under lave punkter for at gøre måttens overflade lige eller flyt til en anden overflade.", + "caption": "Denne måtte har en lineal underneden, hvilket får linjerne til at bue i midten. En svag bue er ok men en stor bue vil give problemer." + }, + "dimensionsSwapped": { + "title": "Rektangler i stedet for kvadrater?", + "description": "Hvis det viste gitter ligner rektangler i stedet for kvadrater, er der måske byttet om på bredde og højde. Byt de to tal ud i felterne.", + "caption": "Bredde og højde er i de forkerte bokse. Der skal stå \"B: 24\" og \"H: 18\"." + }, + "close": "Luk" + }, + "General": { + "close": "Luk", + "error": "Fejl" + }, + "ScaleMenu": { + "scale": "Skalér mønster", + "hide": "Skjul skaleringsmenu", + "show": "Vis skaleringsmenu" + }, + "PdfViewer": { + "error": "Indlæsning af mønster mislykkedes", + "noData": "Tryk på \"Åbn\"-knappen for at åbne et mønster" } } diff --git a/messages/en.json b/messages/en.json index b141bd2e..59947318 100644 --- a/messages/en.json +++ b/messages/en.json @@ -26,7 +26,7 @@ "calibration": { "title": "Calibration", "start": "Click (or tap) “Start Calibrating.”", - "fullscreen": "Enter full screen mode by clicking (or tapping) ", + "fullscreen": "Enter full screen mode by clicking (or tapping) in the top left corner.", "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.”" @@ -125,6 +125,16 @@ "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." + }, + "length": { + "title": "Line length", + "description": "Displays the length of the selected line (shown in purple). Other lines are orange.", + "use": "Tap the length to enter a precise measurement and adjust the line's size. Useful for lengthening/shortening pattern pieces. See this video for an example." + }, + "angle": { + "title": "Line angle", + "description": "Displays the angle of the selected line (shown in purple). Other lines are orange.", + "use": "Enter a precise angle to rotate the line and pattern piece. Use the align to center button to apply the rotation based on the entered angle." } }, "overlayOptions": { diff --git a/messages/fi.json b/messages/fi.json new file mode 100644 index 00000000..c48784d9 --- /dev/null +++ b/messages/fi.json @@ -0,0 +1,344 @@ +{ + "HomePage": { + "youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr", + "github": "Katso lähde koodi", + "calibrate": "Aloita kalibrointi", + "choose-language": "Kieli", + "welcome": { + "title": "Tervetuloa Pattern Projektoriin!", + "description": "Kuvioprojektori on ilmainen ja avoimen lähdekoodin verkkosovellus, joka kalibroi projektorit nopeasti kaavojen ompelua varten. Siinä on myös työkaluja useiden sivujen yhdistämiseen, viivan paksuuden muuttamiseen, värien kääntämiseen, kuvioiden kääntämiseen/kiertämiseen ja muuhun. Voit lukea uusimmista päivityksistä muutoslokista." + }, + "requirements": { + "title": "Mitä tarvitset", + "projector": "Projektorin: vähintäin 720p tarkkuutta suositellaan", + "mat": "Leikkuumatto (optionaalinen)", + "mount": "Kolmiojalka tai seinä-/hylly-/pöytäteline projektorille", + "computer": "Tietokone tai tabletti, jolla käyttää projektoria", + "pattern": "PDF ompelukaavan" + }, + "setup": { + "title": "Asennus", + "place": "Aseta projektori leikkuualustan yläpuolelle osoittamaan suoraan leikkuualustaa. Jos sinulla ei ole leikkuualustaa, voit merkitä ruudukon/suorakulmion pöydälle tai lattialle paperilla tai maalarinteipillä.", + "connect": "Liitä tietokone tai tabletti projektoriin ja joko kopio näytöt tai laajenna näyttöä niin, että tietokoneen tai tabletin kuva näkyy projektorissa.", + "focus": "Säädä projektorin tarkennusta, kunnes teksti on terävää kuvan keskellä. Jos et saa selkeää kuvaa, varmista, että projektorin ja leikkausmaton välinen etäisyys on valmistajan suositteleman toiminta-alueen sisällä.", + "keystone": "Jos projektorissasi on trapetsikorjaus, säädä sitä niin, että kuva on lähes suorakulmainen ja reunojen tarkennus paranee." + }, + "calibration": { + "title": "Kalibrointi", + "start": "Napsauta (tai napauta) ”Aloita kalibrointi.”", + "fullscreen": "Siirry koko näytön tilaan napsauttamalla (tai napauttamalla) vasemmassa yläkulmassa.", + "drag": "Vedä ruudukon kulmia linjaan leikkuumattosi kanssa. Pidä katse matossa ja säädä kulmia tietokoneellasi tai tabletillasi. Säädä kulmien sijoittelua, kunnes kuvan ruudukko vastaa omaa ruudukkoasi.", + "size": "Sinun ei tarvitse kalibroida koko mattoa käyttäen, vaan valitse suurin alue, johon kalibrointiruudukko mahtuu, ja syötä leveys ja korkeus vastaamaan ruudukon leveyttä ja korkeutta.", + "project": "Kun kuvan ruudukko on linjassa leikkuumattosi kanssa, napsauta (tai napauta) ”Heijasta kaava.”" + }, + "project": { + "title": "Heijasta kaava", + "open": "Avaa kaava napsauttamalla (tai napauttamalla) .", + "move": "Siirrä PDF-kaavaa napsauttamalla ja vetämällä sitä näytöllä.", + "cut": "Leikkaa kaavan viivoja pitkin.", + "tools": "Haijasta-tilassa on useita työkaluja. Joillakin on pikanäppäin, joka on merkitty suluissa.", + "fullscreen": { + "title": "Koko näyttö", + "description": "Ohjelmaa on yleensä helpompi käyttää koko näytön tilassa." + }, + "showMenu": { + "title": "Näytä/piilota valikko", + "description": "Näytä tai piilota ylävalikko." + }, + "invert": { + "title": "Käänteiset värit", + "description": "Heijastattaessa vihreät tai valkoiset viivat on yleensä helpompi nähdä mustalla pohjalla. Napsauta/napauta kerran saadaksesi vihreät viivat, kaksi kertaa valkoiset viivat ja kolme kertaa palataksesi mustiin viivoihin." + }, + "moveTool": { + "title": "Näytä tai piilota siirtotyökalu", + "description": "Siirtotyökalussa on neljä nuolipainiketta kalibrointiruudukon kulmien/reunojen siirtämiseen. Siinä on myös keskellä seuraava-painike, jolla voi siirtyä seuraavaan nurkkaan/reunaan." + }, + "overlayOptions": { + "title": "Näytä tai piilota aputaso asetukset", + "description": "Kaavan mukana voidaan näyttää ruudukko, reunus, paperiarkki, kääntöviivat tai nurjan puolen peittokuvat kalibroinnin ja leikkaamisen helpottamiseksi. Lisätietoja on aputaso-asetukset-osiossa." + }, + "lineWeight": { + "title": "Viivan leveys", + "description": "Muuta PDF-kaavan viivojen paksuutta." + }, + "flip": { + "title": "Käännä pystysuunnassa (V) tai vaakasuunnassa (H)", + "description": "Hyödyllinen kaavan puolittaisena peilikuvana." + }, + "rotate": { + "title": "Käännä (R)", + "description": "Käännä kaavan suuntaa 90 astetta." + }, + "recenter": { + "title": "Keskitä ja resetoi (C)", + "description": "Keskittää kaavan leikkausmatolle ja nollaa kääntämisen/pyöräytyksen." + }, + "magnify": { + "title": "Suurenna (M)", + "description": "Napsauta (tai napauta) PDF-kaavaa suurentaaksesi sitä. Napsauta (tai napauta) uudelleen lopettaaksesi suurennus." + }, + "zoomOut": { + "title": "Loitonna (Z)", + "description": "Loitonna nähdäksesi koko PDF-kaavan. Napsauta (tai napauta) PDF-kaavaa lähentääksesi takaisin valittuun kohtaan." + }, + "layers": { + "title": "Näytä tai piilota tasot", + "description": "Näytä tai piilota PDF-kaavan tasot." + }, + "stitch": { + "title": "Näytä tai piilota ommelvalikko", + "description": "Näytä tai piilota ommelvalikko, jonka avulla voit ommella yhteen useita PDF-kaava sivuja." + }, + "scale": { + "title": "Näytä tai piilota skaalausvalikko", + "description": "Muuta kuvion kokoa syöttämällä kerroin: 0–1 pienentää kuviota ja suurempi kuin 1 suurentaa sitä." + }, + "measure": { + "title": "Viivatyökalu (L)", + "description": "Merkitse PDF-kaavaan viivoja kiertämistä, kääntämistä, siirtämistä, mittaamista tai merkitsemistä varten. Katso viivatyökalun yksityiskohtainen kuvaus viivatyökalu-osiosta." + } + }, + "tools": "Työkalut", + "lineTool": { + "title": "Viivatyökalu", + "description": "Viivatyökalulla voi mitata etäisyyksiä, merkitä viivoja PDF-kaavaan, kiertää, kääntää ja siirtää sitä. Napsauta (tai napauta) viivatyökalun painiketta ja napsauta (tai napauta) sitten PDF-kaavaa ja vedä piirtääksesi viivan. Kun viiva on piirretty, näytön alareunaan ilmestyy valikko, jossa on seuraavat vaihtoehdot:", + "delete": { + "title": "Poista viiva", + "description": "Poistaa valitun viivan." + }, + "rotate": { + "title": "Keskitä", + "description": "Kiertää PDF-kaavaa niin, että valittu viiva on vaakasuorassa ja keskitetty leikkuumattoon.", + "use": "Kaavan suuntaamiseksi kankaan langan suuntaan: piirrä viiva langan suunnan suuntaan ja napsauta (tai napauta) sitten Keskitä-painiketta." + }, + "previousNext": { + "title": "Tuo edellinen/seuraava viiva keskelle", + "description": "Siirtää PDF-kaavaa niin, että edellinen/seuraava piirretty viiva on vaakasuorassa ja keskitetty leikkuumattoon.", + "use": "Leikkaamisen aikana kaavojan välillä liikkuminen: piirrä viiva jokaisen kaavan langansuunta ja siirry sitten kaavoja läpi näillä painikkeilla. Poista viivoja liikkuessasi kaavojen läpi, jotta voit seurata, mitkä on jäljellä olevia leikattavia paloja." + }, + "flip": { + "title": "Käännä viivaa pitkin", + "description": "Kääntää PDF-kaavan viivaa pitkin.", + "use": "Kaavojen avaaminen taitokselta: piirrä viiva kaavan taitosviivalle, leikkaa kappale taitosviivaan asti, napsauta (tai napauta) Käännä viivaa pitkin -painiketta ja leikkaa sitten loppuosa kaavasta." + }, + "move": { + "title": "Siirrä PDF-kaava viivan pituuden verran", + "description": "Siirtää PDF-kaavaa viivan pituuden verran.", + "use": "Kaavojen pidentäminen/lyhentäminen: piirrä viiva, joka on kohtisuorassa kaavann pidennys-/lyhennysviivan kanssa, leikkaa pidennys-/lyhennysviivan kohdalle, napsauta (tai napauta) sitten \"Siirrä viivan pituuden mukaan\" -painiketta ja jatka kappaleen leikkaamista. Piirretyn viivan suunta riippuu siitä, pidennätkö vai lyhennätkö kaavaa." + }, + "length": { + "title": "Viivan pituus", + "description": "Näyttää valitun viivan pituuden (näkyy violettina). Muut viivat ovat oransseja.", + "use": "Napauta pituutta syöttääksesi tarkan mitan ja säätääksesi viivan pituutta. Hyödyllinen kaavojen pidentämiseen/lyhentämiseen. Katso esimerkki tästä videosta." + }, + "angle": { + "title": "Viivan kulma", + "description": "Näyttää valitun viivan kulman (näkyy violettina). Muut viivat ovat oransseja.", + "use": "Syötä tarkka kulma viivan ja kaavan kiertämiseksi. Käytä -painiketta keskittääksesi kierron syötetyn kulman perusteella." + } + }, + "overlayOptions": { + "title": "Aputasojen asetukset", + "description": "Aputasojen asetuksia voidaan käyttää kalibroinnin tarkistamiseen kaavan heijastamisen aikana ja leikkauskuvioiden apuna. Seuraavat asetukset ovat käytettävissä:", + "border": { + "title": "Reuna", + "description": "Näyttää kalibrointiruudukon reunan." + }, + "grid": { + "title": "Ruudut", + "description": "Näyttää kalibrointiruudukon." + }, + "paper": { + "title": "Paperiarkki", + "description": "Näyttää Letter- tai A4-kokoisen paperiarkin kokoisen suorakulmion kalibroinnin varmistamiseksi." + }, + "flipLines": { + "title": "Kääntöviivat", + "description": "Näyttää viivoja, jotka auttavat kaavojen taitoksen avaamisessa. Kohdista kaavan taitosviiva käännekohdan kanssa ja paina \"Käännä vaakasuoraan\" tai \"Pystysuoraan\" peilataksesi kappaleen." + }, + "flippedPattern": { + "title": "Nurjapuoli", + "description": "Näyttää pisteitä, kun kaavan nurja puoli heijastetaan." + } + }, + "faq": { + "title": "UKK", + "wrongSizePdf": { + "question": "Onko PDF-kaavasi heijastettu liian pieneksi vai suureksi?", + "answer": "Kalibrointityökalussa ei ole zoomausta, koska kaavan mittakaava tulee PDF-kaavan kokotiedoista. Alunperin kaavoissa on oikea mittakaava, joten mittakaavan muutos on saattanut tapahtua kuviota avattaessa Affinity Designerissa tai Inkscapessa." + }, + "saveAsApp": { + "answer": "Pattern Projector on progressiivinen verkkosovellus (PWA), joten sen voi tallentaa sovelluksena. Tietokoneella voit asentaa sen Chromella tai Edgellä. Tabletilla avaa Jaa-valikko ja napauta Lisää aloitusnäyttöön." + }, + "mobileSupport": { + "question": "Onko ohjelmalla tukea matkapuhelimille ja tableteille?", + "answer": "Vaikka ohjelman käyttö älypuhelimella on mahdollista, näytön rajallinen koko tekee käytöstä hankalaa." + } + }, + "resources": { + "title": "Lisäominaisuudet", + "links": { + "projectorsForSewing": { + "title": "Projectors for Sewing (Projektorit ompeluun) -Facebook-ryhmä", + "link": "https://www.facebook.com/groups/481078582801085" + }, + "onePageGuide": { + "title": "Yhden sivun opas projektorin ompeluun", + "link": "https://bit.ly/onepageguidetoprojectorsewing" + } + } + }, + "contribute": { + "title": "Osallistu projektiin", + "donation": "Jos Pattern Projector on säästänyt sinulta rahaa kopiointipalvelussa tai vapauttanut sinut PDF-katseluohjelman tilauksesta, harkitse kahvin ostamista minulle tai tukemista PayPalin kautta!", + "develop": "Auta GitHubissa ominaisuuksien toteuttamisessa ja ongelmien korjaamisessa.", + "translate": "Käännä tämä työkalu useammille kielille käyttämällä Weblate-komentoa.", + "feedback": "Palaute ja ominaisuuspyynnöt ovat tervetulleita!" + }, + "contact": "Ota yhteyttä" + }, + "InstallButton": { + "title": "Asenna ohjelma", + "description": "Parhaan käyttökokemuksen saat asentamalla Pattern Projectorin Google Chromella. Lataa sovellus Chromessa napauttamalla osoiterivillä.", + "descriptionAndroid": "Parhaan käyttökokemuksen saat Androidilla Firefoxilla. Lataa sovellus napauttamalla selaimen valikossa Lisää-painiketta ja valitsemalla sitten Lisää aloitusnäyttöön.", + "descriptionIOS": "Lataa sovellus napauttamalla jakopainiketta selaimen valikossa ja sitten Lisää aloitusnäyttöön .", + "ok": "OK" + }, + "Mail": { + "title": "Sinulle on postia", + "donate": "Lahjoita" + }, + "Troubleshooting": { + "notMatching": "Eivätkö viivat eivät vastaa leikkuumattoasi?", + "title": "Kalibroinnin vianmääritys", + "dragCorners": { + "title": "Kuinka kalibroidaan", + "description": "Vedä ruudukon kulmat vähintään 61x40 cm:n kokoiseksi suorakulmioksi matolle. Ruudukon tulisi olla mahdollisimman suuri, mutta sen ei tarvitse peittää koko mattoa.", + "caption": "Oikein kalibroitu ruudukko." + }, + "inputMeasurement": "Mittaa leikkuumattosi ruudukon leveys ja korkeus mittanauhalla. Syötä nämä mitat leveys- ja korkeuskenttiin.", + "offByOne": { + "title": "Viivat ovat suoria, mutta eivät ole linjassa?", + "description": "Jos viivat ovat suoria, mutta eivät ole linjassa leikkuumattosi kanssa, leveys tai korkeus saattaa olla 1 cm:n virheellinen. Tarkista mittasi mittanauhalla. Maton mitat voivat olla hämmentäviä tai epätarkkoja, joten mittanauhalla tarkistaminen on tärkeää.", + "caption": "Syötetty on väärät mitat (23 x 17) eikä (24 x 18). Matto näyttää numerot vain lukuihin 23 ja 17 asti, joten mittoja on helppo sekoittaa. Muista mitata mittanauhalla!" + }, + "unevenSurface": { + "title": "Kaarevia viivoja?", + "description": "Jos viivat näyttävät kaarevilta, matto tai maton alla oleva pinta ei ehkä ole tasainen. Yritä tasoittaa mattoa, aseta kartonkia matalien kohtien alle tai siirry eri leikkuualustalle.", + "caption": "Tässä leikkuumatossa on viivain, joka saa viivat kaareutumaan keskeltä. Pieni kaarevuus on ok, mutta suuri kaarevuus aiheuttaa ongelmia." + }, + "dimensionsSwapped": { + "title": "Suorakulmioita neliöiden sijaan?", + "description": "Jos ruudukko näyttää suorakulmioilta neliöiden sijaan, leveys ja korkeus voidaan vaihtaa. Vaihda vain kenttien arvot.", + "caption": "Leveys ja korkeus ovat väärissä ruuduissa. Mittojen pitäisi olla 'L: 24' ja 'K: 18'." + }, + "close": "Sulje" + }, + "General": { + "close": "Sulje", + "error": "Virhe" + }, + "Header": { + "openPDF": "Avaa", + "height": "K:", + "width": "L:", + "project": "Heijasta", + "calibrate": "Kalibroi", + "delete": "Resetoi kalibrointi", + "gridOn": "Ruudut päälle", + "gridOff": "Ruudut pois", + "overlayOptions": "Aputaso asetukset", + "overlayOptionBorder": "Reunat", + "overlayOptionDisabled": "Pois päältä", + "overlayOptionGrid": "Ruudut", + "overlayOptionPaper": "Paperiarkki", + "overlayOptionFliplines": "Kääntöviivat", + "overlayOptionFlippedPattern": "Nurjapuoli", + "invertColor": "Käännä värit", + "invertColorOff": "Värien kääntö pois", + "flipVertical": "Käännä pystysuunnassa (V)", + "flipHorizontal": "Käännä vaakasuunnassa (H)", + "flipCenterOn": "Käännä keskelle", + "flipCenterOff": "Käännä keskelle pois", + "fourCornersOn": "Korosta kaikki kulmat", + "fourCornersOff": "Korosta vain valittu kulma", + "rotate90": "Kierrä 90 astetta myötäpäivään (R)", + "arrowBack": "Sivu takaisin", + "arrowForward": "Sivu eteenpäin", + "info": "Infosivu", + "recenter": "Keskitä ja resetoi (C)", + "fullscreen": "Avaa koko näyttöön", + "fullscreenExit": "Pois koko näytöstä", + "ok": "OK", + "calibrationAlertTitle": "Kalibrointivaroitus", + "calibrationAlert": "Ikkunan koko on muuttunut aloittamisen jälkeen. Korjaa ongelma painamalla koko näytön painiketta tai kalibroimalla uudelleen.", + "calibrationAlertContinue": "Ikkunan kokoa ei voi muuttaa kalibroinnin jälkeen. Jos haluat kuvan koko näytöllä, kalibroi koko näytöllä. Jos haluat muuttaa ikkunan kokoa, kalibroi uudelleen.", + "continue": "Tallenna ikkunan koko ja jatka", + "checkCalibration": "Tarkista kalibrointi", + "menuShow": "Näytä valikko", + "menuHide": "Kätke valikko", + "lineWeight": "Viivan leveys", + "stitchMenuShow": "Näytä ommelvalikko", + "stitchMenuHide": "Ommelvalikko pois", + "stitchMenuDisabled": "Avaa PDF-kaava käyttääksesi ommelvalikkoa", + "measure": "Viivatyökalu (L)", + "showMovement": "Näytä siirtotyökalu", + "hideMovement": "Siirtotyökalu pois", + "magnify": "Suurenna (M)", + "zoomOut": "Pienennä (Z)", + "mail": "Lähetä postia Courtneylle", + "invalidCalibration": "Kalibrointiruudukko ei ole kelvollinen. Yritä uudelleen painamalla 'Resetoi kalibrointi'." + }, + "ScaleMenu": { + "scale": "Skaalaa kaava", + "hide": "Skaalavalikko pois", + "show": "Näytä Skaalavalikko" + }, + "StitchMenu": { + "columnCount": "Sarakkeet", + "rowCount": "Rivit", + "pageRange": "Ompele sivut", + "horizontal": "Vaakasuora päällekkäisyys", + "vertical": "Pystysuora päällekkäisyys", + "zeros": "Lisää nollat tyhjille sivuille, esim. 1-3,0,0,4" + }, + "SaveMenu": { + "save": "Tallenna PDF", + "saveTooltip": "Vie PDF-tiedosto yhdistettyine sivuineen ja valituine tasoineen", + "encryptedPDF": "Tämä PDF-tiedosto on salattu. Syötä salasana tallentaaksesi yhdistetyn PDF-tiedoston." + }, + "LayerMenu": { + "title": "Tasot", + "showAll": "Näytä kaikki", + "hideAll": "Kaikki pois", + "layersOn": "Näytä tasovalikko", + "layersOff": "Tasovalikko pois", + "noLayers": "Kaavassa ei ole tasoja" + }, + "MovementPad": { + "up": "Ylös", + "down": "Alas", + "left": "Vasemmalle", + "right": "Oikealle", + "next": "Seuraava kulma" + }, + "MeasureCanvas": { + "rotateToHorizontal": "Keskitä", + "rotateAndCenterPrevious": "Tuo edellinen rivi keskelle", + "rotateAndCenterNext": "Tuo seuraava rivi keskelle", + "deleteLine": "Poista viiva", + "flipAlong": "Käännä viivaa pitkin", + "translate": "Siirrä kaavaa viivan pituuden verran", + "line": "viiva", + "lines": "viivat" + }, + "OverlayCanvas": { + "zoomedOut": "napsauta kaavaa zoomataksesi", + "magnifying": "napsauta kaavaa zoomauksen lopettamiseksi", + "scaled": "× skaalaa" + }, + "PdfViewer": { + "error": "Kaavan lataaminen epäonnistui", + "noData": "Lataa kaava painamalla \"Avaa\"-painiketta" + } +} diff --git a/messages/pt-BR.json b/messages/pt-BR.json new file mode 100644 index 00000000..bf62751b --- /dev/null +++ b/messages/pt-BR.json @@ -0,0 +1,334 @@ +{ + "HomePage": { + "github": "Veja o código fonte", + "calibrate": "Iniciar calibragem", + "choose-language": "Idioma", + "welcome": { + "title": "Bem vindo ao Projetor de Moldes Pattern Projector!", + "description": "O Pattern projector é um aplicativo web gratuito e de código aberto que calibra rapidamente projetores para moldes de costura. Ele também possui ferramentas para unir moldes de várias páginas, alterar a espessura das linhas, inverter cores, girar/espelhar moldes e muito mais. Para ler sobre as atualizações mais recentes, confira o registro de alterações." + }, + "requirements": { + "title": "O que você vai precisar", + "projector": "Projetor: resolução mínima recomendada de 720p", + "mat": "Um tapete de corte com linhas de grade (opcional)", + "mount": "Tripé ou suporte de parede/prateleira/mesa para projetor", + "computer": "Computador ou tablet para conectar ao projetor", + "pattern": "Um molde de costura em PDF" + }, + "setup": { + "title": "Configuração", + "place": "Coloque o projetor acima da base de corte, apontando diretamente para ela. Se você não tiver uma base de corte, pode usar papel ou fita crepe para marcar uma grade/retângulo em uma mesa ou no chão.", + "connect": "Conecte o computador ou tablet ao projetor e escolha espelhar ou estender a tela para que a imagem apareça no projetor.", + "focus": "Ajuste o foco do projetor até que o texto fique nítido no centro da projeção. Se não conseguir obter uma imagem clara, verifique se a distância entre o projetor e a base de corte está dentro da faixa funcional recomendada pelo fabricante.", + "keystone": "Se o seu projetor tiver correção de keystone, ajuste-a para que a projeção fique o mais próxima possível de um retângulo e o foco nas bordas melhore." + }, + "calibration": { + "title": "Calibração", + "start": "Clique (ou toque) em “Iniciar calibração.”", + "fullscreen": "Entre no modo de tela cheia clicando (ou tocando). ", + "drag": "Arraste os cantos da grade para alinhar com a sua base de corte. Com os olhos na base, ajuste os cantos no seu computador ou tablet. Continue ajustando até que a grade projetada corresponda à grade da sua base de corte.", + "size": "Você não precisa calibrar usando toda a sua base de corte; em vez disso, escolha a maior área em que consiga encaixar a grade de calibração e insira a largura e a altura correspondentes à largura e à altura da grade.", + "project": "Quando a grade projetada estiver alinhada com a sua base de corte, clique (ou toque) em “Projetar.”" + }, + "project": { + "title": "Projetando um molde", + "open": "Clique (ou toque) em para abrir um molde.", + "move": "Mova o PDF clicando e arrastando-o pela tela.", + "cut": "Corte seguindo o desenho projetado.", + "tools": "No modo de projeção, várias ferramentas são disponibilizadas. Algumas têm atalhos de teclado, indicados entre parênteses.", + "fullscreen": { + "title": "Tela cheia", + "description": "Em geral, é mais fácil usar o software no modo de tela cheia." + }, + "showMenu": { + "title": "Mostrar/ocultar menu", + "description": "Mostrar ou ocultar o menu superior." + }, + "invert": { + "title": "Inverter cor", + "description": "Ao projetar, geralmente é mais fácil enxergar linhas verdes ou brancas sobre o fundo preto. Clique/Toque uma vez para linhas verdes, duas vezes para linhas brancas e três vezes para voltar às linhas pretas." + }, + "moveTool": { + "title": "Mostrar ou ocultar a ferramenta de mover", + "description": "A ferramenta de mover possui quatro botões de seta para deslocar os cantos/bordas da grade de calibração. Ela também tem um botão central de avançar para alternar para o próximo canto/borda." + }, + "overlayOptions": { + "title": "Mostrar ou ocultar opções de sobreposição", + "description": "Grade, borda, folha de papel, linhas de inversão ou sobreposições do lado avesso podem ser exibidas no PDF para ajudar na calibração e no corte. Para mais informações, consulte a seção de opções de sobreposição." + }, + "lineWeight": { + "title": "Espessura da linha", + "description": "Alterar a espessura das linhas no molde em PDF." + }, + "flip": { + "title": "Inverter verticalmente (V) ou horizontalmente (H)", + "description": "Útil ao espelhar moldes pela metade." + }, + "rotate": { + "title": "Girar (R)", + "description": "Alterar a orientação do molde em 90 graus." + }, + "recenter": { + "title": "Centralizar e Redefinir (C)", + "description": "Centraliza o molde na base de corte e redefine a rotação/inversão." + }, + "magnify": { + "title": "Ampliar (M)", + "description": "Clique (ou toque) no PDF para ampliá-lo. Clique (ou toque) novamente para parar de ampliar." + }, + "zoomOut": { + "title": "Reduzir zoom (Z)", + "description": "Reduza o zoom para ver o PDF inteiro. Clique (ou toque) no PDF para ampliar novamente no local selecionado." + }, + "layers": { + "title": "Mostrar ou ocultar camadas", + "description": "Mostrar ou ocultar camadas no PDF." + }, + "stitch": { + "title": "Mostrar ou ocultar menu de pontos", + "description": "Mostrar ou ocultar o menu de pontos, que permite unir moldes em PDF de várias páginas." + }, + "scale": { + "title": "Mostrar ou ocultar o menu de escala", + "description": "Altere o tamanho do molde inserindo um multiplicador: entre 0 e 1 deixará o molde menor e maior que 1 deixará o molde maior." + }, + "measure": { + "title": "Ferramenta linha (L)", + "description": "Marque linhas no PDF para girar, inverter, mover, medir ou marcar. Para uma descrição detalhada da ferramenta de linha, consulte a seção da ferramenta de linha." + } + }, + "tools": "Ferramentas", + "lineTool": { + "title": "Ferramenta de linha", + "description": "A ferramenta de linha pode ser usada para medir distâncias, marcar linhas, girar, inverter e mover o PDF. Clique (ou toque) no botão da ferramenta de linha, depois clique (ou toque) no PDF e arraste para desenhar uma linha. Assim que a linha for desenhada, aparecerá um menu na parte inferior da tela com as seguintes opções:", + "delete": { + "title": "Excluir linha", + "description": "Exclui a linha selecionada." + }, + "rotate": { + "title": "Alinhar ao centro", + "description": "Gira o PDF de modo que a linha selecionada fique horizontal e centralizada na sua base de corte.", + "use": "Para alinhar o fio de um molde com o fio do seu tecido: desenhe uma linha na linha do fio do molde e, em seguida, clique (ou toque) no botão alinhar ao centro." + }, + "previousNext": { + "title": "Trazer linha anterior/próxima para o centro", + "description": "Move o PDF de modo que a linha anterior/próxima desenhada fique horizontal e centralizada na sua base de corte.", + "use": "Para alternar entre os moldes durante o corte: desenhe uma linha no fio de cada molde e depois navegue pelos moldes usando esses botões. Exclua as linhas à medida que avançar pelos moldes para acompanhar quais peças ainda precisam ser cortadas." + }, + "flip": { + "title": "Inverter ao longo da linha", + "description": "Inverte o PDF ao longo da linha.", + "use": "Para desdobrar peças do molde: desenhe uma linha na linha de dobra de uma peça do molde, corte a peça até a linha de dobra, clique (ou toque) no botão inverter ao longo da linha e depois corte o restante da peça." + }, + "move": { + "title": "Mover PDF pelo comprimento da linha", + "description": "Move o PDF pelo comprimento da linha.", + "use": "Para alongar/encurtar peças do molde: desenhe uma linha perpendicular à linha de alongar/encurtar de uma peça do molde, corte até a linha de alongar/encurtar, depois clique (ou toque) no botão mover pelo comprimento da linha e então continue cortando a peça. A direção da linha desenhada dependerá se você está alongando ou encurtando a peça do molde." + } + }, + "overlayOptions": { + "title": "Opções de sobreposição", + "description": "As opções de sobreposição podem ser usadas para verificar a calibração durante a projeção e ajudar no corte dos moldes. As seguintes opções estão disponíveis:", + "border": { + "title": "Borda", + "description": "Mostra a borda da grade de calibração." + }, + "grid": { + "title": "Grade", + "description": "Exibe a grade de calibração." + }, + "paper": { + "title": "Folha de papel", + "description": "Mostra um retângulo no tamanho de uma folha Letter ou A4 para verificar a calibração." + }, + "flipLines": { + "title": "Linhas de inversão", + "description": "Mostra linhas que ajudam a desdobrar moldes. Alinhe a linha de dobra do molde com a linha de inversão e pressione inverter horizontalmente ou verticalmente para espelhar a peça." + }, + "flippedPattern": { + "title": "Lado avesso", + "description": "Mostra pontos quando o lado avesso do molde é projetado." + } + }, + "faq": { + "title": "Perguntas frequentes", + "wrongSizePdf": { + "question": "Seu PDF está sendo projetado muito pequeno ou muito grande?", + "answer": "A ferramenta de calibração do Pattern Projector não possui zoom porque a escala da projeção vem das informações de tamanho do PDF. Os moldes de designers geralmente têm a escala correta, portanto, uma alteração na escala pode ter sido introduzida ao abrir o molde no Affinity Designer ou no Inkscape." + }, + "saveAsApp": { + "answer": "O Pattern Projector é um Progressive Web App (PWA), portanto pode ser salvo como um aplicativo. No computador, você pode instalá-lo usando o Chrome ou o Edge. Em um tablet, abra o menu Compartilhar e toque em Adicionar à Tela de Início." + }, + "mobileSupport": { + "question": "Vocês oferecem suporte para celulares e tablets?", + "answer": "Embora seja possível acessar e usar a página em um smartphone, o tamanho limitado da tela torna o uso difícil." + } + }, + "resources": { + "title": "Recursos adicionais", + "links": { + "projectorsForSewing": { + "title": "Grupo do Facebook “Projectors for Sewing”", + "link": "https://www.facebook.com/groups/481078582801085" + }, + "onePageGuide": { + "title": "Guia de uma página para costura com projetor", + "link": "https://bit.ly/onepageguidetoprojectorsewing" + } + } + }, + "contribute": { + "title": "Contribuir para o projeto", + "donation": "Se o Pattern Projector fez você economizar dinheiro na copiadora ou livrou você de uma assinatura de visualizador de PDF, considere me pagar um café ou apoiar via PayPal!", + "develop": "Ajude a implementar recursos e corrigir problemas no GitHub.", + "translate": "Traduza esta ferramenta para mais idiomas usando o Weblate.", + "feedback": "Sugestões e pedidos de recursos são bem-vindos!" + }, + "contact": "Contato", + "youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr" + }, + "InstallButton": { + "title": "Instalar Aplicativo", + "description": "Para a melhor experiência, instale o Pattern Projector usando o Google Chrome. No Chrome, baixe o aplicativo tocando em na barra de endereços.", + "descriptionAndroid": "Para a melhor experiência no Android, use o Firefox. Baixe o aplicativo tocando no botão de mais no menu do navegador e depois em Adicionar à tela inicial.", + "descriptionIOS": "Baixe o aplicativo tocando no botão de compartilhar no menu do navegador e depois em Adicionar à tela inicial .", + "ok": "OK" + }, + "Mail": { + "title": "Você tem uma nova mensagem", + "donate": "Doar" + }, + "Troubleshooting": { + "notMatching": "As linhas não estão coincidindo com a sua base de corte?", + "title": "Problemas de calibração", + "dragCorners": { + "title": "Como calibrar", + "description": "Arraste os cantos da grade até formar um retângulo de pelo menos 24x16 polegadas na sua base de corte. A grade deve ser o maior possível, mas não precisa cobrir toda a base.", + "caption": "Uma grade calibrada corretamente." + }, + "inputMeasurement": "Use uma fita métrica para medir a largura e a altura da grade na sua base de corte. Insira essas medidas nos campos de largura e altura.", + "offByOne": { + "title": "As linhas estão retas, mas não se alinham?", + "description": "Se as linhas projetadas estiverem retas, mas não se alinharem com a sua base de corte, a largura ou a altura podem estar com uma diferença de 1 polegada/centímetro. Verifique novamente suas medidas com uma fita métrica. As medidas da base podem ser confusas ou imprecisas, por isso é importante conferir com a fita métrica.", + "caption": "As medidas foram inseridas incorretamente (23 x 17) em vez de (24 x 18). A base só mostra números até 23 e 17, então é fácil confundir as medidas. Certifique-se de medir com uma fita métrica!" + }, + "unevenSurface": { + "title": "Linhas curvas?", + "description": "Se as linhas projetadas parecerem curvas, sua base ou a superfície abaixo dela pode não estar plana. Tente nivelar a base colocando cartolina nos pontos mais baixos ou mude para uma superfície de corte diferente.", + "caption": "Esta base tem uma régua embaixo, o que faz as linhas se curvarem no centro. Uma leve curvatura não tem problema, mas uma curvatura acentuada causará erros." + }, + "dimensionsSwapped": { + "title": "Retângulos em vez de quadrados?", + "description": "Se a grade projetada parecer composta por retângulos em vez de quadrados, a largura e a altura podem ter sido trocadas. Basta inverter os valores nos campos.", + "caption": "A largura e a altura estão nos campos errados. As dimensões devem ficar como “L: 24” e “A: 18”." + }, + "close": "Fechar" + }, + "General": { + "close": "Fechar", + "error": "Erro" + }, + "Header": { + "openPDF": "Abrir", + "height": "A:", + "width": "L:", + "project": "Projetar", + "calibrate": "Calibrar", + "delete": "Resetar calibração", + "gridOn": "Exibir grade", + "gridOff": "Não exibir grade", + "overlayOptions": "Opções de sobreposição", + "overlayOptionBorder": "Borda", + "overlayOptionDisabled": "Desabilitado", + "overlayOptionGrid": "Grade", + "overlayOptionPaper": "Folha de papel", + "overlayOptionFliplines": "Linhas de espelhamento", + "overlayOptionFlippedPattern": "Lado avesso", + "invertColor": "Inverter cores", + "invertColorOff": "Desligar inverter cores", + "flipVertical": "Inverter verticalmente (V)", + "flipHorizontal": "Inverter Horizontalmente (H)", + "flipCenterOn": "Inverter pelo centro", + "flipCenterOff": "Desligar Inverter pelo centro", + "fourCornersOn": "Destacar todos os cantos", + "fourCornersOff": "Destacar apenas o canto selecionado", + "rotate90": "Girar 90 graus no sentido horário (R)", + "arrowBack": "Voltar 1 página", + "arrowForward": "Avançar 1 página", + "info": "Página de informações", + "recenter": "Centralizar e resetar (C)", + "fullscreen": "Iniciar Tela cheia", + "fullscreenExit": "Sair tela cheia", + "ok": "OK", + "calibrationAlertTitle": "Aviso de Calibragem", + "calibrationAlert": "O tamanho da janela mudou desde que a projeção foi iniciada. Pressione o botão de tela cheia ou refaça a calibração para corrigir esse problema.", + "calibrationAlertContinue": "O tamanho da janela não pode mudar após a calibração. Para projetar em tela cheia, calibre em tela cheia. Se quiser alterar o tamanho da janela, refaça a calibração.", + "continue": "Salvar tamanho da janela e continuar", + "checkCalibration": "Verificar calibração", + "menuShow": "Mostrar menu", + "menuHide": "Esconder menu", + "lineWeight": "Espessura da linha", + "stitchMenuShow": "Mostrar menu de pontos", + "stitchMenuHide": "Esconder menu de pontos", + "stitchMenuDisabled": "Abra o PDF para usar o menu de pontos", + "measure": "Ferramenta de linha (L)", + "showMovement": "Exibir mover", + "hideMovement": "Esconder mover", + "magnify": "Aumentar (M)", + "zoomOut": "Afastar (Z)", + "mail": "Mensagem de Courtney", + "invalidCalibration": "A grade de calibração não é válida. Pressione “Redefinir calibração” para tentar novamente." + }, + "ScaleMenu": { + "scale": "Ajustar escala do molde", + "hide": "Ocultar escala do molde", + "show": "Exibir escala do molde" + }, + "StitchMenu": { + "columnCount": "Colunas", + "rowCount": "Linhas", + "pageRange": "Páginas de pontos", + "horizontal": "Sobreposição horizontal", + "vertical": "Sobreposição vertical", + "zeros": "Adicione 0s para páginas em branco, ex.: 1-3,0,0,4" + }, + "SaveMenu": { + "save": "Exportar PDF", + "saveTooltip": "Exportar PDF com páginas unidas e camadas selecionadas", + "encryptedPDF": "Este PDF está criptografado. Insira a senha para salvar o PDF unido." + }, + "LayerMenu": { + "title": "Camadas", + "showAll": "Exibir tudo", + "hideAll": "Esconder tudo", + "layersOn": "Exibir menu de camadas", + "layersOff": "Ocultar menu de camadas", + "noLayers": "Nenhuma camada no molde" + }, + "MovementPad": { + "up": "Cima", + "down": "Baixo", + "left": "Esquerda", + "right": "Direita", + "next": "Proximo canto" + }, + "MeasureCanvas": { + "rotateToHorizontal": "Alinhar ao centro", + "rotateAndCenterPrevious": "Trazer linha anterior ao centro", + "rotateAndCenterNext": "Trazer próxima linha ao centro", + "deleteLine": "Deletar linhar", + "flipAlong": "Inverter linha", + "translate": "Mover molde por comprimento de linha", + "line": "linha", + "lines": "linhas" + }, + "OverlayCanvas": { + "zoomedOut": "Click molde para aproximar", + "magnifying": "clique molde para parar de aproximar", + "scaled": "× Escala" + }, + "PdfViewer": { + "error": "Falha ao carregar o molde", + "noData": "Pressione o botão de Abrir para carregar um molde" + } +} diff --git a/messages/zh-Hans.json b/messages/zh-Hans.json new file mode 100644 index 00000000..9d1d69da --- /dev/null +++ b/messages/zh-Hans.json @@ -0,0 +1,10 @@ +{ + "HomePage": { + "github": "查看源代码", + "calibrate": "开始校准", + "choose-language": "语言", + "welcome": { + "title": "欢迎来到 Pattern Projector!" + } + } +} diff --git a/middleware.ts b/middleware.ts index cb24a86b..788433ca 100644 --- a/middleware.ts +++ b/middleware.ts @@ -8,14 +8,17 @@ export const localeData = { de: "Deutsch", en: "English", es: "Español", + fi: "Suomi", 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", + "pt-BR": "Português (Brasil)", sl: "Slovenščina", sv: "Svenska", ta: "தமிழ்", + "zh-Hans": "简体中文", }; export const locales = Object.keys(localeData); @@ -34,7 +37,7 @@ export const config = { // *** 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*", + "/(cs|da|de|en|es|fi|fr|hu|it|nb-NO|nl|pt-BR|sl|sv|ta|zh-Hans)/:path*", "/calibrate", ], }; diff --git a/package.json b/package.json index 17f681c2..ddce8e9b 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,12 @@ "license": "MIT", "scripts": { "dev": "next dev", - "build": "prettier . --write; next build", + "build": "prettier . --write; playwright test; next build", "start": "next start", "lint": "next lint", "test:unit": "jest", + "test:e2e": "playwright test", + "test:e2e:update": "playwright test --update-snapshots", "cypress:open": "cypress open", "postinstall": "patch-package" }, @@ -41,6 +43,7 @@ "typescript": "^5.4.4" }, "devDependencies": { + "@playwright/test": "^1.56.0", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/typography": "^0.5.10", "@testing-library/jest-dom": "^6.1.3", @@ -62,5 +65,8 @@ }, "prettier": { "trailingComma": "all" + }, + "engines": { + "node": ">=22.0.0" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..fda860e0 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,153 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * See https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + // Directory where tests are located + testDir: "./tests", + + // Timeout for each test in milliseconds (e.g., 30 seconds) + timeout: 30 * 1000, + + // Maximum time to wait for assertions to pass + expect: { + timeout: 5000, + // Tolerance for visual regression testing (0.01 = 1% pixel difference) + // Adjust this value based on how sensitive you want the visual tests to be. + toHaveScreenshot: { maxDiffPixelRatio: 0.01 }, + }, + + // Run tests in files in parallel + fullyParallel: true, + + // Fail the build on CI if you accidentally left test.only in the source code. + forbidOnly: !!process.env.CI, + + // Retry on CI only + retries: process.env.CI ? 2 : 0, + + // Limit the number of workers to prevent resource exhaustion on CI/local machine + workers: process.env.CI ? 1 : undefined, + + // Reporter to use. See https://playwright.dev/docs/test-reporters + reporter: "html", + + // Output directory for test results and artifacts (screenshots, videos, etc.) + outputDir: "test-results/", + + /* ================================================ + Web Server Configuration for Next.js + ================================================ + This starts your Next.js development server before running tests. + */ + webServer: { + // This assumes you have 'next dev' in your package.json scripts + command: "npm run dev", + url: "http://localhost:3000", + timeout: 120 * 1000, // 2 minutes to wait for the server to start + reuseExistingServer: !process.env.CI, // Don't restart the server if one is already running locally + }, + + projects: [ + // =================================== + // 1. Desktop Projects + // =================================== + { + name: "Desktop Chrome", + use: { + browserName: "chromium", // Explicitly set for clarity + ...devices["Desktop Chrome"], + contextOptions: { reducedMotion: "reduce" }, + }, + }, + { + name: "Desktop Firefox", + use: { + browserName: "firefox", + ...devices["Desktop Firefox"], + contextOptions: { reducedMotion: "reduce" }, + }, + }, + { + name: "Desktop Safari", + use: { + browserName: "webkit", + ...devices["Desktop Safari"], + contextOptions: { reducedMotion: "reduce" }, + }, + }, + + // =================================== + // 2. Mobile Portrait Projects + // =================================== + { + name: "Mobile Chrome (Portrait)", + use: { + browserName: "chromium", + ...devices["Pixel 5"], + contextOptions: { reducedMotion: "reduce" }, + }, + }, + { + name: "Mobile Firefox (Portrait)", + use: { + browserName: "firefox", + // We use a custom viewport instead of a device preset to avoid + // inheriting 'chromium' but keep a similar size. + viewport: { width: 393, height: 851 }, // Pixel 5 dimensions + contextOptions: { reducedMotion: "reduce" }, + }, + }, + { + name: "Mobile Safari (Portrait)", + use: { + browserName: "webkit", + ...devices["iPhone 13"], + contextOptions: { reducedMotion: "reduce" }, + }, + }, + + // =================================== + // 3. Mobile Landscape Projects + // =================================== + { + name: "Mobile Chrome (Landscape)", + use: { + browserName: "chromium", + ...devices["Pixel 5"], + viewport: { width: 851, height: 393 }, + contextOptions: { reducedMotion: "reduce" }, + }, + }, + { + name: "Mobile Firefox (Landscape)", + use: { + browserName: "firefox", + viewport: { width: 851, height: 393 }, + contextOptions: { reducedMotion: "reduce" }, + }, + }, + { + name: "Mobile Safari (Landscape)", + use: { + browserName: "webkit", + viewport: { width: 844, height: 390 }, + contextOptions: { reducedMotion: "reduce" }, + }, + }, + ], + + /* ================================================ + Global Use Options + ================================================ + These options are passed to all tests in all projects. + */ + use: { + // Base URL to use in test.goto(). This aligns with the webServer URL. + baseURL: "http://localhost:3000", + + // Capture screenshot/video/trace only on first retry. + trace: "on-first-retry", + }, +}); diff --git a/tests/visual.spec.ts b/tests/visual.spec.ts new file mode 100644 index 00000000..66e5104d --- /dev/null +++ b/tests/visual.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Visual Regression Testing", () => { + test(`Calibrate page`, async ({ page }) => { + await page.goto("/en/calibrate"); + await expect(page.getByTestId("calibration-canvas")).toBeVisible(); + await page.addStyleTag({ + content: + '[data-testid="calibration-canvas"] { visibility: hidden !important; }', + }); + await expect(page).toHaveScreenshot(`calibrate.png`, { + fullPage: true, + }); + }); + + test(`Home page`, async ({ page }) => { + await page.goto("/"); + await expect(page.getByTestId("welcome-title")).toBeVisible(); + await expect(page).toHaveScreenshot(`home.png`, { + fullPage: true, + }); + }); +}); diff --git a/tests/visual.spec.ts-snapshots/calibrate-Desktop-Chrome-darwin.png b/tests/visual.spec.ts-snapshots/calibrate-Desktop-Chrome-darwin.png new file mode 100644 index 00000000..447ec107 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/calibrate-Desktop-Chrome-darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/calibrate-Desktop-Firefox-darwin.png b/tests/visual.spec.ts-snapshots/calibrate-Desktop-Firefox-darwin.png new file mode 100644 index 00000000..3a8d3638 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/calibrate-Desktop-Firefox-darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/calibrate-Desktop-Safari-darwin.png b/tests/visual.spec.ts-snapshots/calibrate-Desktop-Safari-darwin.png new file mode 100644 index 00000000..39dec12a Binary files /dev/null and b/tests/visual.spec.ts-snapshots/calibrate-Desktop-Safari-darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/calibrate-Mobile-Chrome-Landscape--darwin.png b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Chrome-Landscape--darwin.png new file mode 100644 index 00000000..aa0e6a2b Binary files /dev/null and b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Chrome-Landscape--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/calibrate-Mobile-Chrome-Portrait--darwin.png b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Chrome-Portrait--darwin.png new file mode 100644 index 00000000..89acf62b Binary files /dev/null and b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Chrome-Portrait--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/calibrate-Mobile-Firefox-Landscape--darwin.png b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Firefox-Landscape--darwin.png new file mode 100644 index 00000000..05dd4244 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Firefox-Landscape--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/calibrate-Mobile-Firefox-Portrait--darwin.png b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Firefox-Portrait--darwin.png new file mode 100644 index 00000000..cad02a32 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Firefox-Portrait--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/calibrate-Mobile-Safari-Landscape--darwin.png b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Safari-Landscape--darwin.png new file mode 100644 index 00000000..7ee0ca91 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Safari-Landscape--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/calibrate-Mobile-Safari-Portrait--darwin.png b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Safari-Portrait--darwin.png new file mode 100644 index 00000000..31bbcb98 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/calibrate-Mobile-Safari-Portrait--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/home-Desktop-Chrome-darwin.png b/tests/visual.spec.ts-snapshots/home-Desktop-Chrome-darwin.png new file mode 100644 index 00000000..e51771f6 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/home-Desktop-Chrome-darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/home-Desktop-Firefox-darwin.png b/tests/visual.spec.ts-snapshots/home-Desktop-Firefox-darwin.png new file mode 100644 index 00000000..21bdd89c Binary files /dev/null and b/tests/visual.spec.ts-snapshots/home-Desktop-Firefox-darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/home-Desktop-Safari-darwin.png b/tests/visual.spec.ts-snapshots/home-Desktop-Safari-darwin.png new file mode 100644 index 00000000..94f138de Binary files /dev/null and b/tests/visual.spec.ts-snapshots/home-Desktop-Safari-darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/home-Mobile-Chrome-Landscape--darwin.png b/tests/visual.spec.ts-snapshots/home-Mobile-Chrome-Landscape--darwin.png new file mode 100644 index 00000000..eb3c2007 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/home-Mobile-Chrome-Landscape--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/home-Mobile-Chrome-Portrait--darwin.png b/tests/visual.spec.ts-snapshots/home-Mobile-Chrome-Portrait--darwin.png new file mode 100644 index 00000000..bac5c7b4 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/home-Mobile-Chrome-Portrait--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/home-Mobile-Firefox-Landscape--darwin.png b/tests/visual.spec.ts-snapshots/home-Mobile-Firefox-Landscape--darwin.png new file mode 100644 index 00000000..63d6e392 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/home-Mobile-Firefox-Landscape--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/home-Mobile-Firefox-Portrait--darwin.png b/tests/visual.spec.ts-snapshots/home-Mobile-Firefox-Portrait--darwin.png new file mode 100644 index 00000000..daeac38e Binary files /dev/null and b/tests/visual.spec.ts-snapshots/home-Mobile-Firefox-Portrait--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/home-Mobile-Safari-Landscape--darwin.png b/tests/visual.spec.ts-snapshots/home-Mobile-Safari-Landscape--darwin.png new file mode 100644 index 00000000..fdedfbbc Binary files /dev/null and b/tests/visual.spec.ts-snapshots/home-Mobile-Safari-Landscape--darwin.png differ diff --git a/tests/visual.spec.ts-snapshots/home-Mobile-Safari-Portrait--darwin.png b/tests/visual.spec.ts-snapshots/home-Mobile-Safari-Portrait--darwin.png new file mode 100644 index 00000000..89356376 Binary files /dev/null and b/tests/visual.spec.ts-snapshots/home-Mobile-Safari-Portrait--darwin.png differ diff --git a/yarn.lock b/yarn.lock index 66ef372e..dbe05ef9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -871,6 +871,13 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@playwright/test@^1.56.0": + version "1.56.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.0.tgz#891fe101bddf3eee3dd609e7a145f705dc0f3054" + integrity sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg== + dependencies: + playwright "1.56.0" + "@rollup/rollup-android-arm-eabi@4.9.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.1.tgz#beaf518ee45a196448e294ad3f823d2d4576cf35" @@ -2282,9 +2289,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001520, caniuse-lite@^1.0.30001587: - version "1.0.30001667" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz" - integrity sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw== + version "1.0.30001750" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz" + integrity sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ== canvas@^2.11.2: version "2.11.2" @@ -3605,6 +3612,11 @@ fscreen@^1.0.2: resolved "https://registry.yarnpkg.com/fscreen/-/fscreen-1.2.0.tgz#1a8c88e06bc16a07b473ad96196fb06d6657f59e" integrity sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg== +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -5762,6 +5774,20 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +playwright-core@1.56.0: + version "1.56.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.0.tgz#14b40ea436551b0bcefe19c5bfb8d1804c83739c" + integrity sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ== + +playwright@1.56.0: + version "1.56.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.0.tgz#71c533c61da33e95812f8c6fa53960e073548d9a" + integrity sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA== + dependencies: + playwright-core "1.56.0" + optionalDependencies: + fsevents "2.3.2" + possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"