From eeb470eb4a6fe8f2fc55372a139212c151ee17ab Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Thu, 26 Jun 2025 15:00:42 -0400 Subject: [PATCH 01/38] Accept SVG for open with --- app/manifest.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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"], }, }, ], From 7ae73f660572d762ae1c0fe7b6ff81248856b3f4 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Wed, 16 Jul 2025 16:22:26 -0400 Subject: [PATCH 02/38] Change node version to >=22 for Vercel updates --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 17f681c2..73d9fbb4 100644 --- a/package.json +++ b/package.json @@ -62,5 +62,8 @@ }, "prettier": { "trailingComma": "all" + }, + "engines": { + "node": ">=22.0.0" } } From 0403d815a4c41384b8b4e41c54fa870d93fb93b0 Mon Sep 17 00:00:00 2001 From: miiiii Date: Fri, 15 Aug 2025 07:17:08 +0200 Subject: [PATCH 03/38] Added translation using Weblate (Chinese (Simplified Han script)) --- messages/zh_Hans.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 messages/zh_Hans.json diff --git a/messages/zh_Hans.json b/messages/zh_Hans.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/messages/zh_Hans.json @@ -0,0 +1 @@ +{} From 5b832a1705663d57ef459d148709c87e5d63731d Mon Sep 17 00:00:00 2001 From: miiiii Date: Fri, 15 Aug 2025 07:19:35 +0200 Subject: [PATCH 04/38] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 1.8% (4 of 214 strings) Translation: Pattern Projector/Pattern Projector Translate-URL: https://hosted.weblate.org/projects/pattern-projector/pattern-projector/zh_Hans/ --- messages/zh_Hans.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/messages/zh_Hans.json b/messages/zh_Hans.json index 0967ef42..9d1d69da 100644 --- a/messages/zh_Hans.json +++ b/messages/zh_Hans.json @@ -1 +1,10 @@ -{} +{ + "HomePage": { + "github": "查看源代码", + "calibrate": "开始校准", + "choose-language": "语言", + "welcome": { + "title": "欢迎来到 Pattern Projector!" + } + } +} From 7bffe21f34b2e80281ca6096135e95da1c54b5f7 Mon Sep 17 00:00:00 2001 From: alexandra aguiar Date: Mon, 18 Aug 2025 21:00:24 +0200 Subject: [PATCH 05/38] Added translation using Weblate (Portuguese (Brazil)) --- messages/pt_BR.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 messages/pt_BR.json diff --git a/messages/pt_BR.json b/messages/pt_BR.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/messages/pt_BR.json @@ -0,0 +1 @@ +{} From 2030dc68ca07f7b1ba3d6f6b13c77f4fbf8e43cd Mon Sep 17 00:00:00 2001 From: alexandra aguiar Date: Mon, 18 Aug 2025 22:13:49 +0200 Subject: [PATCH 06/38] Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.0% (212 of 214 strings) Translation: Pattern Projector/Pattern Projector Translate-URL: https://hosted.weblate.org/projects/pattern-projector/pattern-projector/pt_BR/ --- messages/pt_BR.json | 334 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 333 insertions(+), 1 deletion(-) diff --git a/messages/pt_BR.json b/messages/pt_BR.json index 0967ef42..6d304121 100644 --- a/messages/pt_BR.json +++ b/messages/pt_BR.json @@ -1 +1,333 @@ -{} +{ + "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 cores", + "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" + }, + "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": "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" + } +} From 308a8b0370b2d9792a1352bb469f04fb50604364 Mon Sep 17 00:00:00 2001 From: alexandra aguiar Date: Mon, 18 Aug 2025 22:43:49 +0200 Subject: [PATCH 07/38] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (214 of 214 strings) Translation: Pattern Projector/Pattern Projector Translate-URL: https://hosted.weblate.org/projects/pattern-projector/pattern-projector/pt_BR/ --- messages/pt_BR.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/messages/pt_BR.json b/messages/pt_BR.json index 6d304121..bf62751b 100644 --- a/messages/pt_BR.json +++ b/messages/pt_BR.json @@ -45,7 +45,7 @@ "description": "Mostrar ou ocultar o menu superior." }, "invert": { - "title": "Inverter cores", + "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": { @@ -184,7 +184,8 @@ "translate": "Traduza esta ferramenta para mais idiomas usando o Weblate.", "feedback": "Sugestões e pedidos de recursos são bem-vindos!" }, - "contact": "Contato" + "contact": "Contato", + "youTubeSrc": "https://www.youtube.com/embed/videoseries?si=80RVInkOM45wCyMB&list=PLz35rzAwtPHC0IvLaBdWZWaYQxcr4sMlr" }, "InstallButton": { "title": "Instalar Aplicativo", @@ -256,7 +257,7 @@ "arrowForward": "Avançar 1 página", "info": "Página de informações", "recenter": "Centralizar e resetar (C)", - "fullscreen": "Tela cheia", + "fullscreen": "Iniciar Tela cheia", "fullscreenExit": "Sair tela cheia", "ok": "OK", "calibrationAlertTitle": "Aviso de Calibragem", From e8d96c21bfa4662a352b8ff82ae2344ac17aabfa Mon Sep 17 00:00:00 2001 From: Anne Lindholt Date: Mon, 8 Sep 2025 01:41:36 +0200 Subject: [PATCH 08/38] Translated using Weblate (Danish) Currently translated at 100.0% (214 of 214 strings) Translation: Pattern Projector/Pattern Projector Translate-URL: https://hosted.weblate.org/projects/pattern-projector/pattern-projector/da/ --- messages/da.json | 90 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 15 deletions(-) diff --git a/messages/da.json b/messages/da.json index 7a6a0046..874f8e42 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,7 +288,7 @@ "LayerMenu": { "layersOn": "Vis lagmenu", "layersOff": "Skjul lagmenu", - "noLayers": "Ingen lag i PDF", + "noLayers": "Ingen lag i mønsteret", "showAll": "Vis alle", "hideAll": "Skjul all", "title": "Lag" @@ -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" } } From b64bc3f01e4651b14ccdc2d670311f30e56e062f Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Thu, 11 Sep 2025 11:14:49 -0400 Subject: [PATCH 09/38] Add line length and angle text input --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 043f9c21..c2d4d4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Input line length and angle using textboxes + +### Fixed + +- Menus will not hide when inputting text + ## [1.3.0] - 2025-06-25 ### Added From 4cf0aaf986fe07630c37bd974fe92f0a0d5af1b2 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Thu, 11 Sep 2025 11:15:14 -0400 Subject: [PATCH 10/38] Add line length and angle text input --- app/[locale]/calibrate/page.tsx | 5 +- .../canvases/calibration-canvas.tsx | 3 +- app/_components/canvases/measure-canvas.tsx | 129 +++++----- app/_components/canvases/overlay-canvas.tsx | 3 +- app/_components/draggable.tsx | 6 +- app/_components/header.tsx | 16 +- app/_components/inline-input.tsx | 2 +- app/_components/menus/layer-menu.tsx | 2 +- app/_components/menus/line-menu.tsx | 220 +++++++++++------ app/_hooks/use-transform-context.tsx | 2 +- app/_lib/drawing.ts | 28 ++- app/_lib/geometry.ts | 33 ++- app/_lib/interfaces/line.ts | 3 - app/_lib/unit.ts | 9 +- app/_reducers/layersReducer.ts | 33 +-- app/_reducers/linesReducer.ts | 225 ++++++++++++++++++ app/_reducers/localTransformReducer.ts | 86 ++----- app/_reducers/patternScaleReducer.ts | 14 +- app/_reducers/pointsReducer.ts | 15 +- app/_reducers/stitchSettingsReducer.ts | 51 +--- 20 files changed, 541 insertions(+), 344 deletions(-) delete mode 100644 app/_lib/interfaces/line.ts create mode 100644 app/_reducers/linesReducer.ts diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index cb18ef20..6a08ec36 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"; @@ -115,7 +115,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); @@ -521,6 +521,7 @@ export default function Page() {
diff --git a/app/_components/canvases/calibration-canvas.tsx b/app/_components/canvases/calibration-canvas.tsx index a7498eda..47785b1b 100644 --- a/app/_components/canvases/calibration-canvas.tsx +++ b/app/_components/canvases/calibration-canvas.tsx @@ -21,6 +21,7 @@ import { PointAction } from "@/_reducers/pointsReducer"; import { FullScreenHandle } from "react-full-screen"; import Matrix from "ml-matrix"; import { getCalibrationContextUpdatedWithEvent } from "@/_lib/calibration-context"; +import { Unit } from "@/_lib/unit"; const maxPoints = 4; // One point per vertex in rectangle const cornerMargin = 96; @@ -44,7 +45,7 @@ export default function CalibrationCanvas({ width: number; height: number; isCalibrating: boolean; - unitOfMeasure: string; + unitOfMeasure: Unit; displaySettings: DisplaySettings; corners: Set; setCorners: Dispatch>>; diff --git a/app/_components/canvases/measure-canvas.tsx b/app/_components/canvases/measure-canvas.tsx index 4389eb4f..80d7a978 100644 --- a/app/_components/canvases/measure-canvas.tsx +++ b/app/_components/canvases/measure-canvas.tsx @@ -1,11 +1,4 @@ -import { - angleDeg, - constrained, - dist, - distToLine, - transformLine, - transformPoint, -} from "@/_lib/geometry"; +import { constrained, dist, distToLine, transformPoint } from "@/_lib/geometry"; import { CSS_PIXELS_PER_INCH } from "@/_lib/pixels-per-inch"; import { Point } from "@/_lib/point"; import Matrix, { inverse } from "ml-matrix"; @@ -13,19 +6,24 @@ import React, { Dispatch, SetStateAction, useEffect, + useReducer, useRef, useState, } from "react"; import { drawLine, drawArrow } from "@/_lib/drawing"; import { useTransformContext } from "@/_hooks/use-transform-context"; -import { Line } from "@/_lib/interfaces/line"; import { KeyCode } from "@/_lib/key-code"; import LineMenu from "@/_components/menus/line-menu"; import { useKeyDown } from "@/_hooks/use-key-down"; import { useKeyUp } from "@/_hooks/use-key-up"; -import { CM } from "@/_lib/unit"; +import { Unit } from "@/_lib/unit"; import { MenuStates } from "@/_lib/menu-states"; +import linesReducer, { + Line, + createLine, + transformLine, +} from "@/_reducers/linesReducer"; export default function MeasureCanvas({ perspective, @@ -44,7 +42,7 @@ export default function MeasureCanvas({ }: { perspective: Matrix; calibrationTransform: Matrix; - unitOfMeasure: string; + unitOfMeasure: Unit; className?: string; measuring: boolean; setMeasuring: Dispatch>; @@ -60,7 +58,7 @@ export default function MeasureCanvas({ const dragOffset = useRef(null); const [selectedLine, setSelectedLine] = useState(-1); - const [lines, setLines] = useState([]); // pattern space coordinates. + const [lines, dispatchLines] = useReducer(linesReducer, []); const [axisConstrained, setAxisConstrained] = useState(false); const transform = useTransformContext(); @@ -80,11 +78,11 @@ export default function MeasureCanvas({ const patternLine = lines[i]; const clientLine = transformLine(patternLine, patternToClient); // Start dragging one end of the selected line? - if (selectedLine == i) { + if (selectedLine === i) { let minDist = lineTouchRadius; let minEnd = -1; for (const end of [0, 1]) { - const clientEnd = clientLine[end]; + const clientEnd = clientLine.points[end]; const d = dist(clientEnd, client); if (d < minDist) { minDist = d; @@ -96,18 +94,23 @@ export default function MeasureCanvas({ } } if (minEnd >= 0) { - if (minEnd == 0) { + if (minEnd === 0) { // Swap to always drag the end. - setLines(lines.toSpliced(i, 1, [patternLine[1], patternLine[0]])); + dispatchLines({ + type: "update-both-points", + index: i, + newP0: patternLine.points[1], + newP1: patternLine.points[0], + }); } e.stopPropagation(); return; } } - if (distToLine(clientLine, client) < endCircleRadius) { + if (distToLine(clientLine.points, client) < endCircleRadius) { // select/deselect the line. - setSelectedLine(i == selectedLine ? -1 : i); + setSelectedLine(i === selectedLine ? -1 : i); e.stopPropagation(); return; } @@ -123,7 +126,10 @@ export default function MeasureCanvas({ // Create a new line and start dragging its end. const pattern = transformPoint(client, inverse(patternToClient)); - setLines([...lines, [pattern, pattern]]); + dispatchLines({ + type: "add", + line: createLine(pattern, pattern, unitOfMeasure), + }); setSelectedLine(lines.length); dragOffset.current = { x: 0, @@ -149,14 +155,21 @@ export default function MeasureCanvas({ x: client.x + dragOffset.current.x, y: client.y + dragOffset.current.y, }; - const clientToPattern = inverse(transform).mmul(perspective); - const patternDestination = transformPoint( - clientDestination, - clientToPattern, - ); - const patternLine = lines[selectedLine]; - patternLine[1] = patternDestination; - setLines(lines.toSpliced(selectedLine, 1, patternLine)); + + const matLine = transformLine(lines[selectedLine], transform); + let matFinal = transformPoint(clientDestination, perspective); + if (axisConstrained) { + matFinal = constrained(matFinal, matLine.points[0]); + } + const patternDestination = transformPoint(matFinal, inverse(transform)); + + dispatchLines({ + type: "update-point", + index: selectedLine, + pointIndex: 1, // Always dragging the second point + newPoint: patternDestination, + isConstrained: axisConstrained, + }); } }; @@ -176,7 +189,7 @@ export default function MeasureCanvas({ e.stopPropagation(); // finish the line. const patternLine = lines[selectedLine]; - const patternAnchor = patternLine[0]; + const patternAnchor = patternLine.points[0]; const matAnchor = transformPoint(patternAnchor, transform); const destMat = transformPoint(client, perspective); let matFinal = destMat; @@ -191,14 +204,19 @@ export default function MeasureCanvas({ if (!zoomedOut) { setMeasuring(false); } - patternLine[1] = patternFinal; - setLines(lines.toSpliced(selectedLine, 1, patternLine)); + dispatchLines({ + type: "update-point", + index: selectedLine, + pointIndex: 1, + newPoint: patternFinal, + isConstrained: axisConstrained, + }); }; function handleDeleteLine() { if (selectedLine >= 0) { - setLines(lines.toSpliced(selectedLine, 1)); - if (selectedLine == 0) { + dispatchLines({ type: "remove", index: selectedLine }); + if (selectedLine === 0) { setSelectedLine(lines.length - 2); } else { setSelectedLine(selectedLine - 1); @@ -220,6 +238,13 @@ export default function MeasureCanvas({ setAxisConstrained(false); }, [KeyCode.Shift]); + useEffect(() => { + dispatchLines({ + type: "update-unit-of-measure", + unitOfMeasure, + }); + }, [unitOfMeasure]); + useEffect(() => { const canvas = canvasRef.current; if (canvas) { @@ -235,16 +260,22 @@ export default function MeasureCanvas({ const patternToClient = calibrationTransform.mmul(transform); for (let i = 0; i < lines.length; i++) { if (i !== selectedLine) { - drawLine(ctx, transformLine(lines[i], patternToClient)); + drawLine(ctx, transformLine(lines[i], patternToClient).points); } } if (lines.length > 0 && selectedLine >= 0) { const patternLine = lines[selectedLine]; const matLine = transformLine(patternLine, transform); if (axisConstrained && dragOffset.current) { - matLine[1] = constrained(matLine[1], matLine[0]); + matLine.points[1] = constrained( + matLine.points[1], + matLine.points[0], + ); } - const clientLine = transformLine(matLine, calibrationTransform); + const clientLine = transformLine( + matLine, + calibrationTransform, + ).points; drawArrow(ctx, clientLine); drawMeasurementsAt(ctx, matLine, clientLine[1]); } @@ -260,7 +291,7 @@ export default function MeasureCanvas({ ctx.font = "24px sans-serif"; ctx.strokeStyle = "#fff"; ctx.fillStyle = "#000"; - const text = measurementsString(line, unitOfMeasure); + const text = `${line.distance}${line.unitOfMeasure.toLocaleLowerCase()} ${line.angle}°`; ctx.lineWidth = 4; const location = { x: p1.x, y: p1.y - endCircleRadius - 8 }; ctx.strokeText(text, location.x, location.y); @@ -279,7 +310,7 @@ export default function MeasureCanvas({ ]); useEffect(() => { - setLines([]); + dispatchLines({ type: "reset" }); setSelectedLine(-1); }, [file]); @@ -310,34 +341,14 @@ export default function MeasureCanvas({ selectedLine={selectedLine} setSelectedLine={setSelectedLine} lines={lines} - setLines={setLines} + dispatchLines={dispatchLines} handleDeleteLine={handleDeleteLine} gridCenter={gridCenter} setMeasuring={setMeasuring} menusHidden={menusHidden} menuStates={menuStates} + unitOfMeasure={unitOfMeasure} /> ); } -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..38c7a36a 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>; @@ -82,7 +82,7 @@ export default function Draggable({ useProgArrowKeyToMatrix( !isCalibrating, - unitOfMeasure === IN ? quarterInchPx : halfCmPx, + unitOfMeasure === Unit.IN ? quarterInchPx : halfCmPx, (matrix) => { transformer.setLocalTransform(matrix.mmul(transform)); }, diff --git a/app/_components/header.tsx b/app/_components/header.tsx index 6bc67c5b..8cbf9bbd 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; @@ -470,7 +470,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" }, ]} />
diff --git a/app/_components/inline-input.tsx b/app/_components/inline-input.tsx index 4fde665e..d5bbe3a4 100644 --- a/app/_components/inline-input.tsx +++ b/app/_components/inline-input.tsx @@ -45,7 +45,7 @@ export default function InlineInput({ >; 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/_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/_lib/drawing.ts b/app/_lib/drawing.ts index 282ee017..ea44c89d 100644 --- a/app/_lib/drawing.ts +++ b/app/_lib/drawing.ts @@ -1,4 +1,4 @@ -import { CM } from "@/_lib/unit"; +import { Unit } from "@/_lib/unit"; import Matrix from "ml-matrix"; import { Point } from "@/_lib/point"; import { @@ -8,14 +8,14 @@ import { } from "@/_lib/display-settings"; import { RestoreTransforms, + SimpleLine, checkIsConcave, rectCorners, - transformLine, transformPoint, transformPoints, + transformSimpleLine, translatePoints, } from "@/_lib/geometry"; -import { Line } from "./interfaces/line"; export class CanvasState { isConcave: boolean = false; @@ -32,7 +32,7 @@ export class CanvasState { public isCalibrating: boolean, public corners: Set, 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); @@ -209,7 +215,7 @@ export function drawCenterLines(cs: CanvasState) { ctx.strokeStyle = "red"; 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; 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/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..7418ba48 --- /dev/null +++ b/app/_reducers/linesReducer.ts @@ -0,0 +1,225 @@ +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 / 90) * 90).toFixed(0); + } + if (angle === "360") angle = "0"; + return { ...line, distance, angle }; +} + +// Calculates a point based on a starting point, distance, and angle. +function calculatePointFromMetrics( + startPoint: Point, + distance: number, + angle: number, +): Point { + const angleInRadians = (angle * Math.PI) / 180; + return { + x: startPoint.x + distance * Math.cos(angleInRadians), + 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. + // This prevents the line's geometry from changing while the user is typing or backspacing. + 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]; + + // Invert the angle to account for the screen's inverted y-axis. + const correctedAngle = 360 - newAngleNum; + + const newP1 = calculatePointFromMetrics( + p0, + preciseDistance * CSS_PIXELS_PER_INCH, + correctedAngle, + ); + + 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, From fa182cd1417c5a756a77f6c97bd27dd75f649aa5 Mon Sep 17 00:00:00 2001 From: Anne Lindholt Date: Mon, 15 Sep 2025 01:14:20 +0200 Subject: [PATCH 11/38] Translated using Weblate (Danish) Currently translated at 100.0% (214 of 214 strings) Translation: Pattern Projector/Pattern Projector Translate-URL: https://hosted.weblate.org/projects/pattern-projector/pattern-projector/da/ --- messages/da.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/da.json b/messages/da.json index 874f8e42..f1a86600 100644 --- a/messages/da.json +++ b/messages/da.json @@ -290,7 +290,7 @@ "layersOff": "Skjul lagmenu", "noLayers": "Ingen lag i mønsteret", "showAll": "Vis alle", - "hideAll": "Skjul all", + "hideAll": "Slå alle fra", "title": "Lag" }, "MovementPad": { From 36430d2705ff5655b628eaf722f021c464d7d442 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Tue, 16 Sep 2025 10:32:24 -0400 Subject: [PATCH 12/38] Add pt-BR and zh-Hans translations --- CHANGELOG.md | 4 ++++ messages/{pt_BR.json => pt-BR.json} | 0 messages/{zh_Hans.json => zh-Hans.json} | 0 middleware.ts | 4 +++- 4 files changed, 7 insertions(+), 1 deletion(-) rename messages/{pt_BR.json => pt-BR.json} (100%) rename messages/{zh_Hans.json => zh-Hans.json} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 043f9c21..a606a70c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Português (Brasil) and 简体中文 translations + ## [1.3.0] - 2025-06-25 ### Added diff --git a/messages/pt_BR.json b/messages/pt-BR.json similarity index 100% rename from messages/pt_BR.json rename to messages/pt-BR.json diff --git a/messages/zh_Hans.json b/messages/zh-Hans.json similarity index 100% rename from messages/zh_Hans.json rename to messages/zh-Hans.json diff --git a/middleware.ts b/middleware.ts index cb24a86b..a33a62fe 100644 --- a/middleware.ts +++ b/middleware.ts @@ -13,9 +13,11 @@ export const localeData = { 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 +36,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|fr|hu|it|nb-NO|nl|pt-BR|sl|sv|ta|zh-Hans)/:path*", "/calibrate", ], }; From 167c93a7b555d5eae84257eee423fb84de908ecb Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Tue, 16 Sep 2025 14:58:48 -0400 Subject: [PATCH 13/38] Allow pointer events behind the calibration warning --- app/[locale]/calibrate/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index 6a08ec36..1ca6470c 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -531,11 +531,11 @@ export default function Page() { className="bg-white dark:bg-black transition-all duration-500 w-screen h-screen" > {showCalibrationAlert ? ( -
+

{t("calibrationAlert")}