Major new stable release. Includes new intersection boolean operation,
diff --git a/res/freedesktop/solvespace-flatpak.desktop.in b/res/freedesktop/solvespace-flatpak.desktop.in
index c80b67ad..f07f2934 100644
--- a/res/freedesktop/solvespace-flatpak.desktop.in
+++ b/res/freedesktop/solvespace-flatpak.desktop.in
@@ -2,7 +2,7 @@
Version=1.0
Name=SolveSpace
Comment=A parametric 2d/3d CAD
-Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace
+Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace %f
MimeType=application/x-solvespace
Icon=com.solvespace.SolveSpace
Type=Application
diff --git a/res/freedesktop/solvespace-snap.desktop b/res/freedesktop/solvespace-snap.desktop
index da0dda13..7beceb04 100644
--- a/res/freedesktop/solvespace-snap.desktop
+++ b/res/freedesktop/solvespace-snap.desktop
@@ -2,7 +2,7 @@
Version=1.0
Name=SolveSpace
Comment=A parametric 2d/3d CAD
-Exec=solvespace
+Exec=solvespace %f
MimeType=application/x-solvespace
Icon=${SNAP}/meta/icons/hicolor/scalable/apps/snap.solvespace.svg
Type=Application
diff --git a/res/freedesktop/solvespace.desktop.in b/res/freedesktop/solvespace.desktop.in
index 87e6863c..e9b78de2 100644
--- a/res/freedesktop/solvespace.desktop.in
+++ b/res/freedesktop/solvespace.desktop.in
@@ -2,7 +2,7 @@
Version=1.0
Name=SolveSpace
Comment=A parametric 2d/3d CAD
-Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace
+Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace %f
MimeType=application/x-solvespace
Icon=solvespace
Type=Application
diff --git a/res/locales.txt b/res/locales.txt
index d94e2c04..377d6575 100644
--- a/res/locales.txt
+++ b/res/locales.txt
@@ -1,5 +1,6 @@
# This file lists the ISO locale codes (ISO 639-1/ISO 3166-1), Windows LCIDs,
# and human-readable names for every culture supported by SolveSpace.
+cs-CZ,1029,Česky
de-DE,0407,Deutsch
en-US,0409,English (US)
fr-FR,040C,Français
@@ -8,3 +9,4 @@ ru-RU,0419,Русский
tr-TR,041F,Türkçe
uk-UA,0422,Українська
zh-CN,0804,简体中文
+ja-JP,0411,日本語
diff --git a/res/locales/cs_CZ.po b/res/locales/cs_CZ.po
new file mode 100644
index 00000000..fdab48fc
--- /dev/null
+++ b/res/locales/cs_CZ.po
@@ -0,0 +1,2281 @@
+# Czech translations for SolveSpace package.
+# Copyright (C) 2022 the SolveSpace authors
+# This file is distributed under the same license as the SolveSpace package.
+# Pavel Stržínek , 2022
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: SolveSpace 3.1\n"
+"Report-Msgid-Bugs-To: phkahler@gmail.com\n"
+"POT-Creation-Date: 2022-06-19 20:16+0200\n"
+"PO-Revision-Date: 2022-06-19 20:16+0200\n"
+"Last-Translator: Pavel Stržínek \n"
+"Language-Team: none\n"
+"Language: cs_CZ\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: clipboard.cpp:309
+msgid ""
+"Cut, paste, and copy work only in a workplane.\n"
+"\n"
+"Activate one with Sketch -> In Workplane."
+msgstr ""
+"Kopírování, vkládání a vyjímání je funkční jen v pracovní rovině.\n"
+"\n"
+"Aktivuj ji přes Náčrt -> V pracovní rovině."
+
+#: clipboard.cpp:326
+msgid "Clipboard is empty; nothing to paste."
+msgstr "Schránka je prázdná; nic ke vložení."
+
+#: clipboard.cpp:373
+msgid "Number of copies to paste must be at least one."
+msgstr "Je třeba zadat alespoň jednu kopii ke vložení."
+
+#: clipboard.cpp:389 textscreens.cpp:833
+msgid "Scale cannot be zero."
+msgstr "Měřítko nemůže být nulové."
+
+#: clipboard.cpp:431
+msgid "Select one point to define origin of rotation."
+msgstr "Vyber jeden bod pro určení počátku otočení."
+
+#: clipboard.cpp:443
+msgid "Select two points to define translation vector."
+msgstr "Vyber dva body pro určení vektoru posunutí."
+
+#: clipboard.cpp:453
+msgid ""
+"Transformation is identity. So all copies will be exactly on top of each "
+"other."
+msgstr ""
+"Transformace je identická. Všechny kopie budou přesně "
+"nad sebou."
+
+#: clipboard.cpp:457
+msgid "Too many items to paste; split this into smaller pastes."
+msgstr "Příliš mnoho položek ke vložení; rozděl je a vlož po částech."
+
+#: clipboard.cpp:462
+msgid "No workplane active."
+msgstr "Není aktivní žádná pracovní rovina."
+
+#: confscreen.cpp:381
+msgid "Bad format: specify coordinates as x, y, z"
+msgstr "Chybný formát: zadej souřadnice jako x, y, z"
+
+#: confscreen.cpp:391 style.cpp:729 textscreens.cpp:864
+msgid "Bad format: specify color as r, g, b"
+msgstr "Chybný formát: zadej barvu jako r, g, b"
+
+#: confscreen.cpp:417
+msgid ""
+"The perspective factor will have no effect until you enable View -> Use "
+"Perspective Projection."
+msgstr ""
+"Faktor perspektivy nebude mít žádný vliv, dokud nezvolíš Pohled -> Použít perspektivní projekci."
+
+#: confscreen.cpp:435 confscreen.cpp:445
+#, c-format
+msgid "Specify between 0 and %d digits after the decimal."
+msgstr "Urči počet platných číslic za desetinnou čárkou mezi 0 a %d."
+
+#: confscreen.cpp:457
+msgid "Export scale must not be zero!"
+msgstr "Měřítko exportu nesmí být nulové!"
+
+#: confscreen.cpp:469
+msgid "Cutter radius offset must not be negative!"
+msgstr "Posunutí poloměru výřezu nesmí být záporné!"
+
+#: confscreen.cpp:528
+msgid "Bad value: autosave interval should be positive"
+msgstr "Chybná hodnota: interval automatického ukládání by měl být kladný"
+
+#: confscreen.cpp:531
+msgid "Bad format: specify interval in integral minutes"
+msgstr "Chybný formát: zadej interval v celých minutách"
+
+#: constraint.cpp:12
+msgctxt "constr-name"
+msgid "pts-coincident"
+msgstr "shoda-bodů"
+
+#: constraint.cpp:13
+msgctxt "constr-name"
+msgid "pt-pt-distance"
+msgstr "vzdálenost-dvou-bodů"
+
+#: constraint.cpp:14
+msgctxt "constr-name"
+msgid "pt-line-distance"
+msgstr "vzdálenost-bodu-a-úsečky"
+
+#: constraint.cpp:15
+msgctxt "constr-name"
+msgid "pt-plane-distance"
+msgstr "vzdálenost-bodu-a-roviny"
+
+#: constraint.cpp:16
+msgctxt "constr-name"
+msgid "pt-face-distance"
+msgstr "vzdálenost-bodu-a-stěny"
+
+#: constraint.cpp:17
+msgctxt "constr-name"
+msgid "proj-pt-pt-distance"
+msgstr "proj-vzdálenost-dvou-bodů"
+
+#: constraint.cpp:18
+msgctxt "constr-name"
+msgid "pt-in-plane"
+msgstr "bod-v-rovině"
+
+#: constraint.cpp:19
+msgctxt "constr-name"
+msgid "pt-on-line"
+msgstr "bod-na-přímce"
+
+#: constraint.cpp:20
+msgctxt "constr-name"
+msgid "pt-on-face"
+msgstr "bod-na-stěně"
+
+#: constraint.cpp:21
+msgctxt "constr-name"
+msgid "eq-length"
+msgstr "shodná-délka"
+
+#: constraint.cpp:22
+msgctxt "constr-name"
+msgid "eq-length-and-pt-ln-dist"
+msgstr "shodná-délka-a-vzdálenost-bodu-a-úsečky"
+
+#: constraint.cpp:23
+msgctxt "constr-name"
+msgid "eq-pt-line-distances"
+msgstr "shodná-vzdálenost-bodu-a-úsečky"
+
+#: constraint.cpp:24
+msgctxt "constr-name"
+msgid "length-ratio"
+msgstr "poměr-délky"
+
+#: constraint.cpp:25
+msgctxt "constr-name"
+msgid "arc-arc-length-ratio"
+msgstr "poměr-délky-dvou-oblouků"
+
+#: constraint.cpp:26
+msgctxt "constr-name"
+msgid "arc-line-length-ratio"
+msgstr "poměr-délky-oblouku-a-úsečky"
+
+#: constraint.cpp:27
+msgctxt "constr-name"
+msgid "length-difference"
+msgstr "rozdíl-délky"
+
+#: constraint.cpp:28
+msgctxt "constr-name"
+msgid "arc-arc-len-difference"
+msgstr "rozdíl-délky-dvou-oblouků"
+
+#: constraint.cpp:29
+msgctxt "constr-name"
+msgid "arc-line-len-difference"
+msgstr "rozdíl-délky-oblouku-a-úsečky"
+
+#: constraint.cpp:30
+msgctxt "constr-name"
+msgid "symmetric"
+msgstr "symetrie"
+
+#: constraint.cpp:31
+msgctxt "constr-name"
+msgid "symmetric-h"
+msgstr "symetrie-h"
+
+#: constraint.cpp:32
+msgctxt "constr-name"
+msgid "symmetric-v"
+msgstr "symetrie-v"
+
+#: constraint.cpp:33
+msgctxt "constr-name"
+msgid "symmetric-line"
+msgstr "symetrie-přímka"
+
+#: constraint.cpp:34
+msgctxt "constr-name"
+msgid "at-midpoint"
+msgstr "ve-středu"
+
+#: constraint.cpp:35
+msgctxt "constr-name"
+msgid "horizontal"
+msgstr "horizontála"
+
+#: constraint.cpp:36
+msgctxt "constr-name"
+msgid "vertical"
+msgstr "vertikála"
+
+#: constraint.cpp:37
+msgctxt "constr-name"
+msgid "diameter"
+msgstr "průměr"
+
+#: constraint.cpp:38
+msgctxt "constr-name"
+msgid "pt-on-circle"
+msgstr "bod-na-kruhu"
+
+#: constraint.cpp:39
+msgctxt "constr-name"
+msgid "same-orientation"
+msgstr "shodná-orientace"
+
+#: constraint.cpp:40
+msgctxt "constr-name"
+msgid "angle"
+msgstr "úhel"
+
+#: constraint.cpp:41
+msgctxt "constr-name"
+msgid "parallel"
+msgstr "rovnoběžnost"
+
+#: constraint.cpp:42
+msgctxt "constr-name"
+msgid "arc-line-tangent"
+msgstr "tečný-oblouk-a-úsečka"
+
+#: constraint.cpp:43
+msgctxt "constr-name"
+msgid "cubic-line-tangent"
+msgstr "tečná-splajna-a-úsečka"
+
+#: constraint.cpp:44
+msgctxt "constr-name"
+msgid "curve-curve-tangent"
+msgstr "tečné-dvě-křivky"
+
+#: constraint.cpp:45
+msgctxt "constr-name"
+msgid "perpendicular"
+msgstr "kolmost"
+
+#: constraint.cpp:46
+msgctxt "constr-name"
+msgid "eq-radius"
+msgstr "shodný-poloměr"
+
+#: constraint.cpp:47
+msgctxt "constr-name"
+msgid "eq-angle"
+msgstr "shodný-úhel"
+
+#: constraint.cpp:48
+msgctxt "constr-name"
+msgid "eq-line-len-arc-len"
+msgstr "shodná-délka-úsečky-a-oblouku"
+
+#: constraint.cpp:49
+msgctxt "constr-name"
+msgid "lock-where-dragged"
+msgstr "zámek-kam-přetažen"
+
+#: constraint.cpp:50
+msgctxt "constr-name"
+msgid "comment"
+msgstr "komentář"
+
+#: constraint.cpp:144
+msgid ""
+"The tangent arc and line segment must share an endpoint. Constrain them with "
+"Constrain -> On Point before constraining tangent."
+msgstr ""
+"Oblouk a tečná úsečka musejí mít společný koncový bod. Omez je nejdříve příkazem "
+"Omezit -> Na bodě / křivce / ploše."
+
+#: constraint.cpp:163
+msgid ""
+"The tangent cubic and line segment must share an endpoint. Constrain them "
+"with Constrain -> On Point before constraining tangent."
+msgstr ""
+"Tečný splajn a úsečka musejí mít společný koncový bod. Omez je nejdříve příkazem "
+"Omezit -> Na bodě / křivce / ploše."
+
+#: constraint.cpp:189
+msgid ""
+"The curves must share an endpoint. Constrain them with Constrain -> On Point "
+"before constraining tangent."
+msgstr ""
+"Křivky musejí mít společný koncový bod. Omez je příkazem Omezit -> V bodě "
+"před omezením tečny."
+
+#: constraint.cpp:238
+msgid ""
+"Bad selection for distance / diameter constraint. This constraint can apply "
+"to:\n"
+"\n"
+" * two points (distance between points)\n"
+" * a line segment (length)\n"
+" * two points and a line segment or normal (projected distance)\n"
+" * a workplane and a point (minimum distance)\n"
+" * a line segment and a point (minimum distance)\n"
+" * a plane face and a point (minimum distance)\n"
+" * a circle or an arc (diameter)\n"
+msgstr ""
+"Chybný výběr pro omezení vzdálenosti / průměru. Toto omezení lze použít "
+"na:\n"
+"\n"
+" * dva body (vzdálenost mezi body)\n"
+" * úsečku (délka)\n"
+" * dva body a úsečku nebo normálu (promítnutá vzdálenost)\n"
+" * pracovní rovinu a bod (minimální vzdálenost)\n"
+" * úsečku a bod (minimální vzdálenost)\n"
+" * rovinnou plochu a bod (minimální vzdálenost)\n"
+" * kružnici nebo oblouk (průměr)\n"
+
+#: constraint.cpp:291
+msgid ""
+"Bad selection for on point / curve / plane constraint. This constraint can "
+"apply to:\n"
+"\n"
+" * two points (points coincident)\n"
+" * a point and a workplane (point in plane)\n"
+" * a point and a line segment (point on line)\n"
+" * a point and a circle or arc (point on curve)\n"
+" * a point and a plane face (point on face)\n"
+msgstr ""
+"Chybný výběr pro omezení na bod / křivku / rovinu. Toto omezení lze "
+"použít na:\n"
+"\n"
+" * dva body (body se shodují)\n"
+" * bod a pracovní rovinu (bod v rovině)\n"
+" * bod a úsečku (bod na přímce)\n"
+" * bod a kružnici nebo oblouk (bod na křivce)\n"
+" * bod a rovinnou plochu (bod na ploše)\n"
+
+#: constraint.cpp:353
+msgid ""
+"Bad selection for equal length / radius constraint. This constraint can "
+"apply to:\n"
+"\n"
+" * two line segments (equal length)\n"
+" * two line segments and two points (equal point-line distances)\n"
+" * a line segment and two points (equal point-line distances)\n"
+" * a line segment, and a point and line segment (point-line distance "
+"equals length)\n"
+" * four line segments or normals (equal angle between A,B and C,D)\n"
+" * three line segments or normals (equal angle between A,B and B,C)\n"
+" * two circles or arcs (equal radius)\n"
+" * a line segment and an arc (line segment length equals arc length)\n"
+msgstr ""
+"Chybný výběr pro omezení stejné délky / poloměru. Toto omezení lze "
+"použít pro:\n"
+"\n"
+" * dvě úsečky (stejné délky)\n"
+" * dvě úsečky a dva body (stejné vzdálenosti bodů od úseček)\n"
+" * úsečku a dva body (stejné vzdálenosti bodů od přímky)\n"
+" * úsečku a bod a úsečku (vzdálenost bodu od přímky "
+"se rovná délce)\n"
+" * čtyři úsečky nebo normály (stejný úhel mezi A,B a C,D)\n"
+" * tři úsečky nebo normály (stejný úhel mezi A,B a B,C)\n"
+" * dvě kružnice nebo oblouky (stejný poloměr)\n"
+" * úsečku a oblouk (délka úsečky se rovná délce oblouku)\n"
+
+#: constraint.cpp:407
+msgid ""
+"Bad selection for length ratio constraint. This constraint can apply to:\n"
+"\n"
+" * two line segments\n"
+" * two arcs\n"
+" * one arc and one line segment\n"
+msgstr ""
+"Chybný výběr pro omezení poměru délky. Toto omezení lze použít pro:\n"
+"\n"
+" * dvě úsečky\n"
+" * dva oblouky\n"
+" * jeden oblouk a jednu úsečku\n"
+
+#: constraint.cpp:441
+msgid ""
+"Bad selection for length difference constraint. This constraint can apply "
+"to:\n"
+"\n"
+" * two line segments\n"
+" * two arcs\n"
+" * one arc and one line segment\n"
+msgstr ""
+"Chybný výběr pro omezení rozdílu délek. Toto omezení lze použít "
+"pro:\n"
+"\n"
+" * dvě úsečky\n"
+" * dva oblouky\n"
+" * jeden oblouk a jednu úsečku\n"
+
+#: constraint.cpp:472
+msgid ""
+"Bad selection for at midpoint constraint. This constraint can apply to:\n"
+"\n"
+" * a line segment and a point (point at midpoint)\n"
+" * a line segment and a workplane (line's midpoint on plane)\n"
+msgstr ""
+"Chybný výběr pro omezení ve středovém bodě. Toto omezení lze použít pro: \n"
+"\n"
+" * úsečku a bod (bod ve středovém bodě)\n"
+" * úsečku a pracovní rovinu (střed úsečky v rovině)\n"
+
+#: constraint.cpp:530
+msgid ""
+"Bad selection for symmetric constraint. This constraint can apply to:\n"
+"\n"
+" * two points or a line segment (symmetric about workplane's coordinate "
+"axis)\n"
+" * line segment, and two points or a line segment (symmetric about line "
+"segment)\n"
+" * workplane, and two points or a line segment (symmetric about "
+"workplane)\n"
+msgstr ""
+"Chybný výběr pro symetrické omezení. Toto omezení lze použít pro:\n"
+"\n"
+" * dva body nebo úsečku (symetrickou vůči souřadnicím osy pracovní roviny) "
+" * úsečku a dva body nebo úsečku (symetrická k úsečce)\n"
+" * pracovní rovinu a dva body nebo úsečku (symetrická k "
+"pracovní rovině)\n"
+
+#: constraint.cpp:545
+msgid ""
+"A workplane must be active when constraining symmetric without an explicit "
+"symmetry plane."
+msgstr ""
+"Pracovní rovina musí být aktivní, pokud je omezena symetricky bez explicitní "
+"symetrické roviny."
+
+#: constraint.cpp:579
+msgid ""
+"Activate a workplane (with Sketch -> In Workplane) before applying a "
+"horizontal or vertical constraint."
+msgstr ""
+"Aktivuj pracovní rovinu (pomocí Náčrt -> V pracovní rovině) před použitím "
+"omezení horizontály nebo vertikály."
+
+#: constraint.cpp:592
+msgid ""
+"Bad selection for horizontal / vertical constraint. This constraint can "
+"apply to:\n"
+"\n"
+" * two points\n"
+" * a line segment\n"
+msgstr ""
+"Chybný výběr pro horizontální / vertikální omezení. Toto omezení lze "
+"použít pro:\n"
+"\n"
+" * dva body\n"
+" * úsečku\n"
+
+#: constraint.cpp:613
+msgid ""
+"Bad selection for same orientation constraint. This constraint can apply "
+"to:\n"
+"\n"
+" * two normals\n"
+msgstr ""
+"Chybný výběr pro omezení stejné orientace. Toto omezení lze použít "
+" pro: \n "
+"\n"
+" * dvě normály\n"
+
+#: constraint.cpp:663
+msgid "Must select an angle constraint."
+msgstr "Je nutné vybrat omezení úhlu."
+
+#: constraint.cpp:676
+msgid "Must select a constraint with associated label."
+msgstr "Je nutné vybrat omezení s přiřazeným štítkem."
+
+#: constraint.cpp:687
+msgid ""
+"Bad selection for angle constraint. This constraint can apply to:\n"
+"\n"
+" * two line segments\n"
+" * a line segment and a normal\n"
+" * two normals\n"
+msgstr ""
+"Chybný výběr pro omezení úhlu. Toto omezení lze použít pro: \n"
+"\n"
+" * dvě úsečky\n"
+" * úsečku a normálu\n"
+" * dvě normály\n"
+
+#: constraint.cpp:754
+msgid "Curve-curve tangency must apply in workplane."
+msgstr "Tečnost dvou křivek musí být použita v pracovní rovině."
+
+#: constraint.cpp:766
+msgid ""
+"Bad selection for parallel / tangent constraint. This constraint can apply "
+"to:\n"
+"\n"
+" * two line segments (parallel)\n"
+" * a line segment and a normal (parallel)\n"
+" * two normals (parallel)\n"
+" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n"
+msgstr ""
+"Chybný výběr pro rovnoběžné / tečné omezení. Toto omezení lze použít "
+" pro: \n "
+"\n"
+" * dvě úsečky (rovnoběžné)\n"
+" * úsečku a normálu (rovnoběžnou)\n"
+" * dvě normály (rovnoběžné)\n"
+" * dvě úsečky, oblouky nebo beziéry, které mají společný koncový bod (tečna)\n"
+
+#: constraint.cpp:784
+msgid ""
+"Bad selection for perpendicular constraint. This constraint can apply to:\n"
+"\n"
+" * two line segments\n"
+" * a line segment and a normal\n"
+" * two normals\n"
+msgstr ""
+"Chybný výběr pro omezení kolmosti. Toto omezení lze použít pro: \n"
+"\n"
+" * dvě úsečky\n"
+" * úsečku a normálu\n"
+" * dvě normály\n"
+
+#: constraint.cpp:799
+msgid ""
+"Bad selection for lock point where dragged constraint. This constraint can "
+"apply to:\n"
+"\n"
+" * a point\n"
+msgstr ""
+"Chybný výběr pro omezení uzamčení v místě přetažení. Toto omezení lze "
+"použít pro: \n"
+"\n"
+" * bod\n"
+
+#: constraint.cpp:813 mouse.cpp:1158
+msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT"
+msgstr "NOVÝ KOMENTÁŘ -- UPRAVIT DVOJKLIKEM"
+
+#: constraint.cpp:818
+msgid "click center of comment text"
+msgstr "klikni na střed textu komentáře"
+
+#: export.cpp:19
+msgid ""
+"No solid model present; draw one with extrudes and revolves, or use Export "
+"2d View to export bare lines and curves."
+msgstr ""
+"Není přítomen žádný model tělesa; vytvoř model pomocí extruzí a rotací nebo "
+"použij Exportovat 2D pohled... pro export prostých čar a křivek."
+
+#: export.cpp:61
+msgid ""
+"Bad selection for export section. Please select:\n"
+"\n"
+" * nothing, with an active workplane (workplane is section plane)\n"
+" * a face (section plane through face)\n"
+" * a point and two line segments (plane through point and parallel to "
+"lines)\n"
+msgstr ""
+"Chybný výběr části pro export. Vyber prosím:\n"
+"\n"
+" * nic, s aktivní pracovní rovinou (pracovní rovina je rovina řezu)\n"
+" * plochu (rovina řezu procházející plochou)\n"
+" * bod a dvě úsečky (rovina procházející bodem a rovnoběžná s "
+" přímkami)\n"
+
+#: export.cpp:818
+msgid "Active group mesh is empty; nothing to export."
+msgstr "Síť aktivní skupiny je prázdná; není co exportovat."
+
+#: exportvector.cpp:336
+msgid "freehand lines were replaced with continuous lines"
+msgstr "volné úsečky byly nahrazeny souvislými"
+
+#: exportvector.cpp:338
+msgid "zigzag lines were replaced with continuous lines"
+msgstr "klikaté úsečky byly nahrazeny souvislými"
+
+#: exportvector.cpp:592
+msgid ""
+"Some aspects of the drawing have no DXF equivalent and were not exported:\n"
+msgstr ""
+"Některé prvky výkresu nemají ekvivalent ve formátu DXF a nebyly exportovány:\n"
+
+#: exportvector.cpp:838
+msgid ""
+"PDF page size exceeds 200 by 200 inches; many viewers may reject this file."
+msgstr ""
+"Velikost stránky PDF přesahuje 200 x 200 palců; mnoho prohlížečů může tento "
+"soubor odmítnout."
+
+#: file.cpp:44 group.cpp:91
+msgctxt "group-name"
+msgid "sketch-in-plane"
+msgstr "náčrt-v-rovině"
+
+#: file.cpp:62
+msgctxt "group-name"
+msgid "#references"
+msgstr "#odkazy"
+
+#: file.cpp:550
+msgid "The file is empty. It may be corrupt."
+msgstr "Soubor je prázdný. Může být poškozený."
+
+#: file.cpp:555
+msgid ""
+"Unrecognized data in file. This file may be corrupt, or from a newer version "
+"of the program."
+msgstr ""
+"Nerozpoznaná data v souboru. Tento soubor může být poškozený nebo pochází z novější verze "
+"programu."
+
+#: file.cpp:867
+msgctxt "title"
+msgid "Missing File"
+msgstr "Chybějící soubor"
+
+#: file.cpp:868
+#, c-format
+msgctxt "dialog"
+msgid "The linked file “%s” is not present."
+msgstr "Odkazovaný soubor “%s” není k dispozici."
+
+#: file.cpp:870
+msgctxt "dialog"
+msgid ""
+"Do you want to locate it manually?\n"
+"\n"
+"If you decline, any geometry that depends on the missing file will be "
+"permanently removed."
+msgstr ""
+"Chceš jej vyhledat ručně?\n"
+"\n"
+"Pokud odmítneš, všechny geometrie, které závisí na chybějícím souboru, budou "
+"trvale odstraněny."
+
+#: file.cpp:873
+msgctxt "button"
+msgid "&Yes"
+msgstr "&Ano"
+
+#: file.cpp:875
+msgctxt "button"
+msgid "&No"
+msgstr "&Ne"
+
+#: file.cpp:877 solvespace.cpp:611
+msgctxt "button"
+msgid "&Cancel"
+msgstr "&Zrušit"
+
+#: graphicswin.cpp:41
+msgid "&File"
+msgstr "&Soubor"
+
+#: graphicswin.cpp:42
+msgid "&New"
+msgstr "&Nový"
+
+#: graphicswin.cpp:43
+msgid "&Open..."
+msgstr "&Otevřít..."
+
+#: graphicswin.cpp:44
+msgid "Open &Recent"
+msgstr "Otevřít &nedávný"
+
+#: graphicswin.cpp:45
+msgid "&Save"
+msgstr "&Uložit"
+
+#: graphicswin.cpp:46
+msgid "Save &As..."
+msgstr "Uložit &jako..."
+
+#: graphicswin.cpp:48
+msgid "Export &Image..."
+msgstr "Exportovat &Obrázek..."
+
+#: graphicswin.cpp:49
+msgid "Export 2d &View..."
+msgstr "Exportovat 2D &pohled..."
+
+#: graphicswin.cpp:50
+msgid "Export 2d &Section..."
+msgstr "Exportovat 2D čá&st..."
+
+#: graphicswin.cpp:51
+msgid "Export 3d &Wireframe..."
+msgstr "Exportovat 3D &drátěný model..."
+
+#: graphicswin.cpp:52
+msgid "Export Triangle &Mesh..."
+msgstr "Exportovat &trojúhelníkovou síť (mesh)..."
+
+#: graphicswin.cpp:53
+msgid "Export &Surfaces..."
+msgstr "Exportovat &povrchy..."
+
+#: graphicswin.cpp:54
+msgid "Im&port..."
+msgstr "&Importovat..."
+
+#: graphicswin.cpp:57
+msgid "E&xit"
+msgstr "U&končit"
+
+#: graphicswin.cpp:60
+msgid "&Edit"
+msgstr "Up&ravit"
+
+#: graphicswin.cpp:61
+msgid "&Undo"
+msgstr "&Zpět"
+
+#: graphicswin.cpp:62
+msgid "&Redo"
+msgstr "Z&novu"
+
+#: graphicswin.cpp:63
+msgid "Re&generate All"
+msgstr "Znovu vše &generovat"
+
+#: graphicswin.cpp:65
+msgid "Snap Selection to &Grid"
+msgstr "Přichytit výběr k &mřížce"
+
+#: graphicswin.cpp:66
+msgid "Rotate Imported &90°"
+msgstr "Otočit importované o &90°"
+
+#: graphicswin.cpp:68
+msgid "Cu&t"
+msgstr "Vyjmou&t"
+
+#: graphicswin.cpp:69
+msgid "&Copy"
+msgstr "&Kopírovat"
+
+#: graphicswin.cpp:70
+msgid "&Paste"
+msgstr "V&ložit"
+
+#: graphicswin.cpp:71
+msgid "Paste &Transformed..."
+msgstr "Vložit &transformované..."
+
+#: graphicswin.cpp:72
+msgid "&Delete"
+msgstr "O&dstranit"
+
+#: graphicswin.cpp:74
+msgid "Select &Edge Chain"
+msgstr "Vybrat ř&etězec hran"
+
+#: graphicswin.cpp:75
+msgid "Select &All"
+msgstr "Vybr&at vše"
+
+#: graphicswin.cpp:76
+msgid "&Unselect All"
+msgstr "Zr&ušit výběr všeho"
+
+#: graphicswin.cpp:78
+msgid "&Line Styles..."
+msgstr "Sty&ly čar..."
+
+#: graphicswin.cpp:79
+msgid "&View Projection..."
+msgstr "&Projekční pohled..."
+
+#: graphicswin.cpp:81
+msgid "Con&figuration..."
+msgstr "Nastave&ní..."
+
+#: graphicswin.cpp:84
+msgid "&View"
+msgstr "&Zobrazení"
+
+#: graphicswin.cpp:85
+msgid "Zoom &In"
+msgstr "Přiblíž&it"
+
+#: graphicswin.cpp:86
+msgid "Zoom &Out"
+msgstr "&Oddálit"
+
+#: graphicswin.cpp:87
+msgid "Zoom To &Fit"
+msgstr "Přiblížit na &míru"
+
+#: graphicswin.cpp:89
+msgid "Align View to &Workplane"
+msgstr "Zarovnat pohled na pracovní rovinu"
+
+#: graphicswin.cpp:90
+msgid "Nearest &Ortho View"
+msgstr "Nejbližší &ortogonální pohled"
+
+#: graphicswin.cpp:91
+msgid "Nearest &Isometric View"
+msgstr "Nejbližší &izometrický pohled"
+
+#: graphicswin.cpp:92
+msgid "&Center View At Point"
+msgstr "&Pohled na střed v bodě"
+
+#: graphicswin.cpp:94
+msgid "Show Snap &Grid"
+msgstr "Zobrazit mřížku přichy&cení"
+
+#: graphicswin.cpp:95
+msgid "Darken Inactive Solids"
+msgstr "Ztmavit neaktivní tělesa"
+
+#: graphicswin.cpp:96
+msgid "Use &Perspective Projection"
+msgstr "Použít &perspektivní projekci"
+
+#: graphicswin.cpp:97
+msgid "Show E&xploded View"
+msgstr "Zobrazit &rozbalený pohled"
+
+#: graphicswin.cpp:98
+msgid "Dimension &Units"
+msgstr "&Jednotky rozměru"
+
+#: graphicswin.cpp:99
+msgid "Dimensions in &Millimeters"
+msgstr "Rozměry v &milimetrech"
+
+#: graphicswin.cpp:100
+msgid "Dimensions in M&eters"
+msgstr "Rozměry v m&etrech"
+
+#: graphicswin.cpp:101
+msgid "Dimensions in &Inches"
+msgstr "Rozměry v &palcích"
+
+#: graphicswin.cpp:102
+msgid "Dimensions in &Feet and Inches"
+msgstr "Rozměry ve &stopách a palcích"
+
+#: graphicswin.cpp:104
+msgid "Show &Toolbar"
+msgstr "Zobrazit panel &nástrojů"
+
+#: graphicswin.cpp:105
+msgid "Show Property Bro&wser"
+msgstr "Zobrazit ¶metry"
+
+#: graphicswin.cpp:107
+msgid "&Full Screen"
+msgstr "Na &celou obrazovku"
+
+#: graphicswin.cpp:109
+msgid "&New Group"
+msgstr "&Nová skupina"
+
+#: graphicswin.cpp:110
+msgid "Sketch In &3d"
+msgstr "Náčrt ve &3D"
+
+#: graphicswin.cpp:111
+msgid "Sketch In New &Workplane"
+msgstr "Náčrt v nové pracovní &rovině"
+
+#: graphicswin.cpp:113
+msgid "Step &Translating"
+msgstr "Krokový &posun"
+
+#: graphicswin.cpp:114
+msgid "Step &Rotating"
+msgstr "Krokové &otočení"
+
+#: graphicswin.cpp:116
+msgid "E&xtrude"
+msgstr "Extruze (e&xtrude)"
+
+#: graphicswin.cpp:117
+msgid "&Helix"
+msgstr "Šroubovice (&helix)"
+
+#: graphicswin.cpp:118
+msgid "&Lathe"
+msgstr "Plná rotace (&lathe)"
+
+#: graphicswin.cpp:119
+msgid "Re&volve"
+msgstr "Volná rotace (re&volve)"
+
+#: graphicswin.cpp:121
+msgid "Link / Assemble..."
+msgstr "Odkaz / Sestavení..."
+
+#: graphicswin.cpp:122
+msgid "Link Recent"
+msgstr "Odkaz na nedávný"
+
+#: graphicswin.cpp:124
+msgid "&Sketch"
+msgstr "&Náčrt"
+
+#: graphicswin.cpp:125
+msgid "In &Workplane"
+msgstr "V &pracovní rovině"
+
+#: graphicswin.cpp:126
+msgid "Anywhere In &3d"
+msgstr "Kdekoliv ve &3D"
+
+#: graphicswin.cpp:128
+msgid "Datum &Point"
+msgstr "Vztažný &Bod"
+
+#: graphicswin.cpp:129
+msgid "&Workplane"
+msgstr "&Pracovní rovina"
+
+#: graphicswin.cpp:131
+msgid "Line &Segment"
+msgstr "Úsečka"
+
+#: graphicswin.cpp:132
+msgid "C&onstruction Line Segment"
+msgstr "Konstrukční úsečka"
+
+#: graphicswin.cpp:133
+msgid "&Rectangle"
+msgstr "&Obdélník"
+
+#: graphicswin.cpp:134
+msgid "&Circle"
+msgstr "&Kružnice"
+
+#: graphicswin.cpp:135
+msgid "&Arc of a Circle"
+msgstr "&Oblouk kružnice"
+
+#: graphicswin.cpp:136
+msgid "&Bezier Cubic Spline"
+msgstr "&Bézierův kubický splajn"
+
+#: graphicswin.cpp:138
+msgid "&Text in TrueType Font"
+msgstr "&Text v písmu TrueType"
+
+#: graphicswin.cpp:139
+msgid "&Image"
+msgstr "&Obrázek"
+
+#: graphicswin.cpp:141
+msgid "To&ggle Construction"
+msgstr "Přepnout &konstrukci"
+
+#: graphicswin.cpp:142
+msgid "Tangent &Arc at Point"
+msgstr "Tečný &oblouk v bodě"
+
+#: graphicswin.cpp:143
+msgid "Split Curves at &Intersection"
+msgstr "Rozdělit kř&ivky v průsečíku"
+
+#: graphicswin.cpp:145
+msgid "&Constrain"
+msgstr "Ome&zení"
+
+#: graphicswin.cpp:146
+msgid "&Distance / Diameter"
+msgstr "&Vzdálenost / průměr"
+
+#: graphicswin.cpp:147
+msgid "Re&ference Dimension"
+msgstr "Re&ferenční rozměr"
+
+#: graphicswin.cpp:148
+msgid "A&ngle"
+msgstr "Úhe&l"
+
+#: graphicswin.cpp:149
+msgid "Reference An&gle"
+msgstr "Refe&renční úhel"
+
+#: graphicswin.cpp:150
+msgid "Other S&upplementary Angle"
+msgstr "Další doplňkový úh&el"
+
+#: graphicswin.cpp:151
+msgid "Toggle R&eference Dim"
+msgstr "Přepnout r&eferenční rozměr"
+
+#: graphicswin.cpp:153
+msgid "&Horizontal"
+msgstr "&Horizontála"
+
+#: graphicswin.cpp:154
+msgid "&Vertical"
+msgstr "&Vertikála"
+
+#: graphicswin.cpp:156
+msgid "&On Point / Curve / Plane"
+msgstr "&Na bodě / křivce / ploše"
+
+#: graphicswin.cpp:157
+msgid "E&qual Length / Radius / Angle"
+msgstr "&Shodná délka / poloměr / úhel"
+
+#: graphicswin.cpp:158
+msgid "Length / Arc Ra&tio"
+msgstr "Poměr délky / oblo&uku"
+
+#: graphicswin.cpp:159
+msgid "Length / Arc Diff&erence"
+msgstr "Rozdíl &délky / oblouku"
+
+#: graphicswin.cpp:160
+msgid "At &Midpoint"
+msgstr "Ve středové&m bodě"
+
+#: graphicswin.cpp:161
+msgid "S&ymmetric"
+msgstr "S&ymetrie"
+
+#: graphicswin.cpp:162
+msgid "Para&llel / Tangent"
+msgstr "Rovno&běžnost / tečna"
+
+#: graphicswin.cpp:163
+msgid "&Perpendicular"
+msgstr "&Kolmost"
+
+#: graphicswin.cpp:164
+msgid "Same Orient&ation"
+msgstr "Shodná orient&ace"
+
+#: graphicswin.cpp:165
+msgid "Lock Point Where &Dragged"
+msgstr "Uzamčení v místě pře&tažení"
+
+#: graphicswin.cpp:167
+msgid "Comment"
+msgstr "Komentář"
+
+#: graphicswin.cpp:169
+msgid "&Analyze"
+msgstr "&Analyzovat"
+
+#: graphicswin.cpp:170
+msgid "Measure &Volume"
+msgstr "Měření &objemu"
+
+#: graphicswin.cpp:171
+msgid "Measure A&rea"
+msgstr "Měření &plochy"
+
+#: graphicswin.cpp:172
+msgid "Measure &Perimeter"
+msgstr "Měření ob&vodu"
+
+#: graphicswin.cpp:173
+msgid "Show &Interfering Parts"
+msgstr "Zobrazit kol&idující části"
+
+#: graphicswin.cpp:174
+msgid "Show &Naked Edges"
+msgstr "Zobrazit ob&nažené hrany"
+
+#: graphicswin.cpp:175
+msgid "Show &Center of Mass"
+msgstr "Zobrazit &těžiště"
+
+#: graphicswin.cpp:177
+msgid "Show &Underconstrained Points"
+msgstr "Zobrazit &nedostatečně omezené body"
+
+#: graphicswin.cpp:179
+msgid "&Trace Point"
+msgstr "&Trasovat bod"
+
+#: graphicswin.cpp:180
+msgid "&Stop Tracing..."
+msgstr "&Zastavit trasování..."
+
+#: graphicswin.cpp:181
+msgid "Step &Dimension..."
+msgstr "&Rozměr kroku..."
+
+#: graphicswin.cpp:183
+msgid "&Help"
+msgstr "&Nápověda"
+
+#: graphicswin.cpp:184
+msgid "&Language"
+msgstr "&Jazyk"
+
+#: graphicswin.cpp:185
+msgid "&Website / Manual"
+msgstr "&Web / Manuál"
+
+#: graphicswin.cpp:186
+msgid "&Go to GitHub commit"
+msgstr "Revize na &GitHubu"
+
+#: graphicswin.cpp:188
+msgid "&About"
+msgstr "O &aplikaci"
+
+#: graphicswin.cpp:362
+msgid "(no recent files)"
+msgstr "(žádné nedávné soubory)"
+
+#: graphicswin.cpp:370
+#, c-format
+msgid "File '%s' does not exist."
+msgstr "Soubor '%s' neexistuje."
+
+#: graphicswin.cpp:737
+msgid "No workplane is active, so the grid will not appear."
+msgstr "Žádná pracovní rovina není aktivní, proto nebude mřížka zobrazena."
+
+#: graphicswin.cpp:752
+msgid ""
+"The perspective factor is set to zero, so the view will always be a parallel "
+"projection.\n"
+"\n"
+"For a perspective projection, modify the perspective factor in the "
+"configuration screen. A value around 0.3 is typical."
+msgstr ""
+"Faktor perspektivy je nastaven na nulu, proto bude pohled vždy rovnoběžnou "
+"projekcí.\n"
+"\n"
+"Pro perspektivní projekci uprav faktor perspektivy v "
+"konfigurační obrazovce. Typická hodnota je kolem 0,3."
+
+#: graphicswin.cpp:837
+msgid ""
+"Select a point; this point will become the center of the view on screen."
+msgstr ""
+"Vyber bod; tento bod se stane středem pohledu na obrazovce."
+
+#: graphicswin.cpp:1137
+msgid "No additional entities share endpoints with the selected entities."
+msgstr "Žádné další entity nesdílejí koncové body s vybranými entitami."
+
+#: graphicswin.cpp:1155
+msgid ""
+"To use this command, select a point or other entity from an linked part, or "
+"make a link group the active group."
+msgstr ""
+"Chceš-li použít tento příkaz, vyber bod nebo jinou entitu z propojené části "
+"nebo vyber skupinu propojení jako aktivní skupinu."
+
+#: graphicswin.cpp:1178
+msgid ""
+"No workplane is active. Activate a workplane (with Sketch -> In Workplane) "
+"to define the plane for the snap grid."
+msgstr ""
+"Žádná pracovní rovina není aktivní. Aktivuj pracovní rovinu (pomocí Náčrt "
+"-> V pracovní rovině) pro nastavení roviny přichytávání na mřížku."
+
+#: graphicswin.cpp:1185
+msgid ""
+"Can't snap these items to grid; select points, text comments, or constraints "
+"with a label. To snap a line, select its endpoints."
+msgstr ""
+"Tyto položky nelze přichytit k mřížce; vyber body, textové komentáře nebo omezení "
+"se štítkem. Chceš-li přichytit úsečku, vyber její koncové body."
+
+#: graphicswin.cpp:1270
+msgid "No workplane selected. Activating default workplane for this group."
+msgstr ""
+"Není vybrána žádná pracovní rovina. Aktivuji výchozí pracovní rovinu pro "
+"tuto skupinu."
+
+#: graphicswin.cpp:1273
+msgid ""
+"No workplane is selected, and the active group does not have a default "
+"workplane. Try selecting a workplane, or activating a sketch-in-new-"
+"workplane group."
+msgstr ""
+"Není vybrána žádná pracovní rovina a aktivní skupina nemá výchozí "
+"pracovní rovinu přiřazenu. Zkus vybrat pracovní rovinu nebo aktivovat "
+"skupinu náčrt-v-rovině."
+
+#: graphicswin.cpp:1294
+msgid ""
+"Bad selection for tangent arc at point. Select a single point, or select "
+"nothing to set up arc parameters."
+msgstr ""
+"Chybný výběr tečného oblouku v bodě. Vyber jeden bod nebo výběr "
+"zruš pro nastavení parametrů oblouku."
+
+#: graphicswin.cpp:1305
+msgid "click point on arc (draws anti-clockwise)"
+msgstr "klikni na bod oblouku (kresleno proti směru hodinových ručiček)"
+
+#: graphicswin.cpp:1306
+msgid "click to place datum point"
+msgstr "klikni pro umístění vztažného bodu"
+
+#: graphicswin.cpp:1307
+msgid "click first point of line segment"
+msgstr "klikni na první bod úsečky"
+
+#: graphicswin.cpp:1309
+msgid "click first point of construction line segment"
+msgstr "klikni na první bod konstrukční úsečky"
+
+#: graphicswin.cpp:1310
+msgid "click first point of cubic segment"
+msgstr "klikni na první bod segmentu splajnu"
+
+#: graphicswin.cpp:1311
+msgid "click center of circle"
+msgstr "klikni na střed kružnice"
+
+#: graphicswin.cpp:1312
+msgid "click origin of workplane"
+msgstr "klikni na počátek pracovní roviny"
+
+#: graphicswin.cpp:1313
+msgid "click one corner of rectangle"
+msgstr "klikni na jeden roh obdélníku"
+
+#: graphicswin.cpp:1314
+msgid "click top left of text"
+msgstr "klikni na levý horní roh textu"
+
+#: graphicswin.cpp:1320
+msgid "click top left of image"
+msgstr "klikni na levý horní roh obrázku"
+
+#: graphicswin.cpp:1346
+msgid ""
+"No entities are selected. Select entities before trying to toggle their "
+"construction state."
+msgstr ""
+"Nejsou vybrány žádné entity. Před přepnutím stavu konstrukce "
+"nějaké entity vyber."
+
+#: group.cpp:86
+msgctxt "group-name"
+msgid "sketch-in-3d"
+msgstr "náčrt-ve-3D"
+
+#: group.cpp:150
+msgid ""
+"Bad selection for new sketch in workplane. This group can be created with:\n"
+"\n"
+" * a point (through the point, orthogonal to coordinate axes)\n"
+" * a point and two line segments (through the point, parallel to the "
+"lines)\n"
+" * a point and a normal (through the point, orthogonal to the normal)\n"
+" * a workplane (copy of the workplane)\n"
+msgstr ""
+"Chybný výběr nového náčrtu v pracovním plánu. Tuto skupinu lze vytvořit pomocí:\n"
+"\n"
+" * bodu (přes bod, kolmo na souřadnicové osy)\n"
+" * bodu a dvou úseček (procházející bodem, rovnoběžná s "
+"přímkami)\n"
+" * bodu a normály (procházející bodem, kolmá na normálu)\n"
+" * pracovní roviny (kopie pracovní roviny)\n"
+
+#: group.cpp:166
+msgid ""
+"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch "
+"will be extruded normal to the workplane."
+msgstr ""
+"Před extruzí aktivuj pracovní rovinu (Náčrt -> V pracovní rovině). Náčrt "
+"bude extrudován ve směru normály k pracovní rovině."
+
+#: group.cpp:175
+msgctxt "group-name"
+msgid "extrude"
+msgstr "extruze"
+
+#: group.cpp:180
+msgid "Lathe operation can only be applied to planar sketches."
+msgstr "Operace plné rotace lze použít pouze na rovinné náčrty.."
+
+#: group.cpp:191
+msgid ""
+"Bad selection for new lathe group. This group can be created with:\n"
+"\n"
+" * a point and a line segment or normal (revolved about an axis parallel "
+"to line / normal, through point)\n"
+" * a line segment (revolved about line segment)\n"
+msgstr ""
+"Chybný výběr pro novou skupinu plné rotace. Tuto skupinu lze vytvořit pomocí:\n"
+"\n"
+" * bodu a úsečky nebo normály (rotací kolem osy rovnoběžné "
+"k přímce / normále, procházející bodem)\n"
+" * úsečky (rotací kolem úsečky)\n"
+
+#: group.cpp:201
+msgctxt "group-name"
+msgid "lathe"
+msgstr "plná-rotace"
+
+#: group.cpp:206
+msgid "Revolve operation can only be applied to planar sketches."
+msgstr "Operaci volné rotace lze použít pouze na rovinné náčrty."
+
+#: group.cpp:217
+msgid ""
+"Bad selection for new revolve group. This group can be created with:\n"
+"\n"
+" * a point and a line segment or normal (revolved about an axis parallel "
+"to line / normal, through point)\n"
+" * a line segment (revolved about line segment)\n"
+msgstr ""
+"Chybný výběr pro novou skupinu volné rotace. Tuto skupinu lze vytvořit pomocí:\n"
+"\n"
+" * bodu a úsečky nebo normály (rotace kolem osy rovnoběžné s "
+"přímkou / normálou, procházející bodem)\n"
+" * úsečky (rotace kolem úsečky)\n"
+
+#: group.cpp:229
+msgctxt "group-name"
+msgid "revolve"
+msgstr "volná-rotace"
+
+#: group.cpp:234
+msgid "Helix operation can only be applied to planar sketches."
+msgstr "Operaci šroubovice lze použít pouze na rovinné náčrty."
+
+#: group.cpp:245
+msgid ""
+"Bad selection for new helix group. This group can be created with:\n"
+"\n"
+" * a point and a line segment or normal (revolved about an axis parallel "
+"to line / normal, through point)\n"
+" * a line segment (revolved about line segment)\n"
+msgstr ""
+"Chybný výběr pro novou skupinu šroubovice. Tuto skupinu lze vytvořit pomocí:\n"
+"\n"
+" * bodu a úsečky nebo normály (rotací kolem osy rovnoběžné "
+"k přímce / normále, procházející bodem)\n"
+" * úsečky (rotací kolem úsečky)\n"
+
+#: group.cpp:257
+msgctxt "group-name"
+msgid "helix"
+msgstr "šroubovice"
+
+#: group.cpp:270
+msgid ""
+"Bad selection for new rotation. This group can be created with:\n"
+"\n"
+" * a point, while locked in workplane (rotate in plane, about that "
+"point)\n"
+" * a point and a line or a normal (rotate about an axis through the "
+"point, and parallel to line / normal)\n"
+msgstr ""
+"Chybný výběr pro nové krokové otočení. Tuto skupinu lze vytvořit pomocí:\n"
+"\n"
+" * bodu, přičemž je uzamčen v pracovní rovině (otočení v rovině, kolem tohoto "
+"bodu)\n"
+" * bodu a úsečky nebo normály (otočení kolem osy procházející "
+"bodem a rovnoběžně s přímkou / normálou)\n"
+
+#: group.cpp:283
+msgctxt "group-name"
+msgid "rotate"
+msgstr "otočení"
+
+#: group.cpp:294
+msgctxt "group-name"
+msgid "translate"
+msgstr "posun"
+
+#: group.cpp:416
+msgid "(unnamed)"
+msgstr "(nepojmenované)"
+
+#: groupmesh.cpp:707
+msgid "not closed contour, or not all same style!"
+msgstr "Obrys není uzavřený nebo není celý v jednotném stylu!"
+
+#: groupmesh.cpp:720
+msgid "points not all coplanar!"
+msgstr "Ne všechny body jsou v rovině!"
+
+#: groupmesh.cpp:722
+msgid "contour is self-intersecting!"
+msgstr "Obrys se sám protíná!"
+
+#: groupmesh.cpp:724
+msgid "zero-length edge!"
+msgstr "Nulová délka hrany!"
+
+#: importmesh.cpp:136
+msgid "Text-formated STL files are not currently supported"
+msgstr "Textové soubory STL nejsou v této chvíli podporovány."
+
+#: modify.cpp:252
+msgid "Must be sketching in workplane to create tangent arc."
+msgstr "Pro vytvoření tečného oblouku je nutné kreslit v pracovní rovině."
+
+#: modify.cpp:299
+msgid ""
+"To create a tangent arc, select a point where two non-construction lines or "
+"circles in this group and workplane join."
+msgstr ""
+"Chceš-li vytvořit tečný oblouk, vyber bod, kde se s pracovní rovinou stýkají "
+"dvě nekonstrukční úsečky nebo kružnice v této skupině."
+
+#: modify.cpp:386
+msgid ""
+"Couldn't round this corner. Try a smaller radius, or try creating the "
+"desired geometry by hand with tangency constraints."
+msgstr ""
+"Tento roh nelze zaoblit. Zkus menší poloměr nebo zkus vytvořit "
+"požadovanou geometrii ručně pomocí tečných omezení."
+
+#: modify.cpp:595
+msgid "Couldn't split this entity; lines, circles, or cubics only."
+msgstr "Tuto entitu se nepodařilo rozdělit; pouze úsečky, kružnice nebo splajny."
+
+#: modify.cpp:622
+msgid "Must be sketching in workplane to split."
+msgstr "Rozdělení lze provést pouze při náčrtu v pracovní rovině."
+
+#: modify.cpp:629
+msgid ""
+"Select two entities that intersect each other (e.g. two lines/circles/arcs "
+"or a line/circle/arc and a point)."
+msgstr ""
+"Vyber dvě entity, které se vzájemně protínají (např. dvě čáry / kružnice / oblouky "
+"nebo úsečka / kružnice / oblouk a bod)."
+
+#: modify.cpp:734
+msgid "Can't split; no intersection found."
+msgstr "Nelze rozdělit, nebyla nalezena žádná průsečnice."
+
+#: mouse.cpp:557
+msgid "Assign to Style"
+msgstr "Přiřadit ke stylu"
+
+#: mouse.cpp:573
+msgid "No Style"
+msgstr "Žádný styl"
+
+#: mouse.cpp:576
+msgid "Newly Created Custom Style..."
+msgstr "Nově vytvořený vlastní styl..."
+
+#: mouse.cpp:583
+msgid "Group Info"
+msgstr "Info o skupině"
+
+#: mouse.cpp:603
+msgid "Style Info"
+msgstr "Info o stylu"
+
+#: mouse.cpp:623
+msgid "Select Edge Chain"
+msgstr "Vybrat řetězec hran"
+
+#: mouse.cpp:629
+msgid "Toggle Reference Dimension"
+msgstr "Přepnout referenční rozměr"
+
+#: mouse.cpp:635
+msgid "Other Supplementary Angle"
+msgstr "Další doplňkový úhel"
+
+#: mouse.cpp:640
+msgid "Snap to Grid"
+msgstr "Přichytit k mřížce"
+
+#: mouse.cpp:649
+msgid "Remove Spline Point"
+msgstr "Odebrat bod splajnu"
+
+#: mouse.cpp:684
+msgid "Add Spline Point"
+msgstr "Přidat bod splajnu"
+
+#: mouse.cpp:688
+msgid "Cannot add spline point: maximum number of points reached."
+msgstr "Nelze přidat bod splajnu: bylo dosaženo maximálního počtu bodů."
+
+#: mouse.cpp:713
+msgid "Toggle Construction"
+msgstr "Přepnout konstrukci"
+
+#: mouse.cpp:729
+msgid "Delete Point-Coincident Constraint"
+msgstr "Odstranit omezení kolidujícího bodu"
+
+#: mouse.cpp:747
+msgid "Cut"
+msgstr "Vyjmout"
+
+#: mouse.cpp:749
+msgid "Copy"
+msgstr "Kopírovat"
+
+#: mouse.cpp:753
+msgid "Select All"
+msgstr "Vybrat vše"
+
+#: mouse.cpp:758
+msgid "Paste"
+msgstr "Vložit"
+
+#: mouse.cpp:760
+msgid "Paste Transformed..."
+msgstr "Vložit transformované..."
+
+#: mouse.cpp:765
+msgid "Delete"
+msgstr "Odstranit"
+
+#: mouse.cpp:768
+msgid "Unselect All"
+msgstr "Zrušit výběr všeho"
+
+#: mouse.cpp:775
+msgid "Unselect Hovered"
+msgstr "Zrušit výběr při najetí"
+
+#: mouse.cpp:784
+msgid "Zoom to Fit"
+msgstr "Přiblížit na míru"
+
+#: mouse.cpp:986 mouse.cpp:1274
+msgid "click next point of line, or press Esc"
+msgstr "klikni na další bod úsečky nebo stiskni klávesu Esc"
+
+#: mouse.cpp:992
+msgid ""
+"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In "
+"Workplane."
+msgstr ""
+"Nelze nakreslit obdélník ve 3D; nejprve aktivuj pracovní rovinu pomocí Náčrt -> V "
+"pracovní rovině."
+
+#: mouse.cpp:1026
+msgid "click to place other corner of rectangle"
+msgstr "kliknutím umísti další roh obdélníku"
+
+#: mouse.cpp:1047
+msgid "click to set radius"
+msgstr "kliknutím nastav poloměr"
+
+#: mouse.cpp:1052
+msgid ""
+"Can't draw arc in 3d; first, activate a workplane with Sketch -> In "
+"Workplane."
+msgstr ""
+"Nelze nakreslit oblouk ve 3D; nejprve aktivuj pracovní rovinu pomocí Náčrt -> V "
+"pracovní rovině."
+
+#: mouse.cpp:1071
+msgid "click to place point"
+msgstr "kliknutím umísti bod"
+
+#: mouse.cpp:1087
+msgid "click next point of cubic, or press Esc"
+msgstr "klikni na další bod splajnu nebo stiskni Esc"
+
+#: mouse.cpp:1092
+msgid ""
+"Sketching in a workplane already; sketch in 3d before creating new workplane."
+msgstr ""
+"Kreslení v pracovní rovině již probíhá; před vytvořením nové pracovní roviny "
+"kresli ve 3D."
+
+#: mouse.cpp:1108
+msgid ""
+"Can't draw text in 3d; first, activate a workplane with Sketch -> In "
+"Workplane."
+msgstr ""
+"Nelze kreslit text ve 3D; nejprve aktivuj pracovní rovinu pomocí Náčrt -> V "
+"pracovní rovině."
+
+#: mouse.cpp:1125
+msgid "click to place bottom right of text"
+msgstr "klikni pro umístění pravého dolního rohu textu"
+
+#: mouse.cpp:1131
+msgid ""
+"Can't draw image in 3d; first, activate a workplane with Sketch -> In "
+"Workplane."
+msgstr ""
+"Nelze nakreslit obrázek ve 3d; nejprve aktivuj pracovní rovinu pomocí Náčrt -> V "
+"pracovní rovině."
+
+#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:553
+msgctxt "file-type"
+msgid "SolveSpace models"
+msgstr "SolveSpace modely"
+
+#: platform/gui.cpp:89
+msgctxt "file-type"
+msgid "ALL"
+msgstr "VŠE"
+
+#: platform/gui.cpp:91
+msgctxt "file-type"
+msgid "IDF circuit board"
+msgstr "IDF deska plošných spojů"
+
+#: platform/gui.cpp:92
+msgctxt "file-type"
+msgid "STL triangle mesh"
+msgstr "STL trojúhelníková síť"
+
+#: platform/gui.cpp:96
+msgctxt "file-type"
+msgid "PNG image"
+msgstr "PNG obrázek"
+
+#: platform/gui.cpp:100
+msgctxt "file-type"
+msgid "STL mesh"
+msgstr "STL síť (mesh)"
+
+#: platform/gui.cpp:101
+msgctxt "file-type"
+msgid "Wavefront OBJ mesh"
+msgstr "Wavefront OBJ síť (mesh)"
+
+#: platform/gui.cpp:102
+msgctxt "file-type"
+msgid "Three.js-compatible mesh, with viewer"
+msgstr "Three.js-compatibilní síť (mesh), s prohlížečem"
+
+#: platform/gui.cpp:103
+msgctxt "file-type"
+msgid "Three.js-compatible mesh, mesh only"
+msgstr "Three.js-compatibilní síť (mesh), bez prohlížeče"
+
+#: platform/gui.cpp:104
+msgctxt "file-type"
+msgid "VRML text file"
+msgstr "VRML textový soubor"
+
+#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122
+msgctxt "file-type"
+msgid "STEP file"
+msgstr "STEP soubor"
+
+#: platform/gui.cpp:112
+msgctxt "file-type"
+msgid "PDF file"
+msgstr "PDF soubor"
+
+#: platform/gui.cpp:113
+msgctxt "file-type"
+msgid "Encapsulated PostScript"
+msgstr "Zapouzdřený PostScript"
+
+#: platform/gui.cpp:114
+msgctxt "file-type"
+msgid "Scalable Vector Graphics"
+msgstr "SVG soubor"
+
+#: platform/gui.cpp:116 platform/gui.cpp:123
+msgctxt "file-type"
+msgid "DXF file (AutoCAD 2007)"
+msgstr "DXF soubor (AutoCAD 2007)"
+
+#: platform/gui.cpp:117
+msgctxt "file-type"
+msgid "HPGL file"
+msgstr "HPGL soubor"
+
+#: platform/gui.cpp:118
+msgctxt "file-type"
+msgid "G Code"
+msgstr "G kód (G Code)"
+
+#: platform/gui.cpp:127
+msgctxt "file-type"
+msgid "AutoCAD DXF and DWG files"
+msgstr "AutoCAD DXF a DWG soubory"
+
+#: platform/gui.cpp:131
+msgctxt "file-type"
+msgid "Comma-separated values"
+msgstr "CSV soubor"
+
+#: platform/guigtk.cpp:1382 platform/guimac.mm:1509 platform/guiwin.cpp:1641
+msgid "untitled"
+msgstr "nepojmenovaný"
+
+#: platform/guigtk.cpp:1393 platform/guigtk.cpp:1426 platform/guimac.mm:1467
+#: platform/guiwin.cpp:1639
+msgctxt "title"
+msgid "Save File"
+msgstr "Uložit soubor"
+
+#: platform/guigtk.cpp:1394 platform/guigtk.cpp:1427 platform/guimac.mm:1450
+#: platform/guiwin.cpp:1645
+msgctxt "title"
+msgid "Open File"
+msgstr "Otevřít soubor"
+
+#: platform/guigtk.cpp:1397 platform/guigtk.cpp:1433
+msgctxt "button"
+msgid "_Cancel"
+msgstr "_Zrušit"
+
+#: platform/guigtk.cpp:1398 platform/guigtk.cpp:1431
+msgctxt "button"
+msgid "_Save"
+msgstr "_Uložit"
+
+#: platform/guigtk.cpp:1399 platform/guigtk.cpp:1432
+msgctxt "button"
+msgid "_Open"
+msgstr "_Otevřít"
+
+#: solvespace.cpp:171
+msgctxt "title"
+msgid "Autosave Available"
+msgstr "Dostupné automatické ukládání"
+
+#: solvespace.cpp:172
+msgctxt "dialog"
+msgid "An autosave file is available for this sketch."
+msgstr "Pro tento náčrt je k dispozici automatické ukládání souboru."
+
+#: solvespace.cpp:173
+msgctxt "dialog"
+msgid "Do you want to load the autosave file instead?"
+msgstr "Chceš místo toho načíst automaticky uložený soubor?"
+
+#: solvespace.cpp:174
+msgctxt "button"
+msgid "&Load autosave"
+msgstr "&Načíst automaticky uložený"
+
+#: solvespace.cpp:176
+msgctxt "button"
+msgid "Do&n't Load"
+msgstr "&Nenačítat"
+
+#: solvespace.cpp:599
+msgctxt "title"
+msgid "Modified File"
+msgstr "Upravený soubor"
+
+#: solvespace.cpp:601
+#, c-format
+msgctxt "dialog"
+msgid "Do you want to save the changes you made to the sketch “%s”?"
+msgstr "Chceš uložit změny, které jsi provedl v náčrtu “%s”?"
+
+#: solvespace.cpp:604
+msgctxt "dialog"
+msgid "Do you want to save the changes you made to the new sketch?"
+msgstr "Chceš provedené změny uložit do nového náčrtu?"
+
+#: solvespace.cpp:607
+msgctxt "dialog"
+msgid "Your changes will be lost if you don't save them."
+msgstr "Pokud změny neuložíš, budou ztraceny."
+
+#: solvespace.cpp:608
+msgctxt "button"
+msgid "&Save"
+msgstr "&Uložit"
+
+#: solvespace.cpp:610
+msgctxt "button"
+msgid "Do&n't Save"
+msgstr "&Neukládat"
+
+#: solvespace.cpp:631
+msgctxt "title"
+msgid "(new sketch)"
+msgstr "(nový náčrt)"
+
+#: solvespace.cpp:638
+msgctxt "title"
+msgid "Property Browser"
+msgstr "Parametry"
+
+#: solvespace.cpp:700
+msgid ""
+"Constraints are currently shown, and will be exported in the toolpath. This "
+"is probably not what you want; hide them by clicking the link at the top of "
+"the text window."
+msgstr ""
+"Omezení jsou aktuálně zobrazena a budou exportována do cesty nástroje. To "
+"pravděpodobně není to, co chceš; skryj je kliknutím na odkaz v horní části "
+"textového okna."
+
+#: solvespace.cpp:772
+#, c-format
+msgid ""
+"Can't identify file type from file extension of filename '%s'; try .dxf or ."
+"dwg."
+msgstr ""
+"Nelze určit typ souboru podle přípony názvu souboru '%s'; zkus .dxf nebo ."
+"dwg."
+
+#: solvespace.cpp:824
+msgid "Constraint must have a label, and must not be a reference dimension."
+msgstr "Omezení musí mít popisek a nesmí být referenčním rozměrem."
+
+#: solvespace.cpp:828
+msgid "Bad selection for step dimension; select a constraint."
+msgstr "Chybný výběr rozměru kroku; vyber omezení."
+
+#: solvespace.cpp:852
+msgid "The assembly does not interfere, good."
+msgstr "V pořádku, sestava se nepřekrývá."
+
+#: solvespace.cpp:868
+#, c-format
+msgid ""
+"The volume of the solid model is:\n"
+"\n"
+" %s"
+msgstr ""
+"Objem modelu tělesa je:\n"
+"\n"
+" %s"
+
+#: solvespace.cpp:877
+#, c-format
+msgid ""
+"\n"
+"The volume of current group mesh is:\n"
+"\n"
+" %s"
+msgstr ""
+"\n"
+"Objem aktuální mesh skupiny je:\n"
+"\n"
+" %s"
+
+#: solvespace.cpp:882
+msgid ""
+"\n"
+"\n"
+"Curved surfaces have been approximated as triangles.\n"
+"This introduces error, typically of around 1%."
+msgstr ""
+"\n"
+"\n"
+"Zakřivené plochy byly aproximovány jako trojúhelníky.\n"
+"To přináší chybu, obvykle kolem 1%%."
+
+#: solvespace.cpp:897
+#, c-format
+msgid ""
+"The surface area of the selected faces is:\n"
+"\n"
+" %s\n"
+"\n"
+"Curves have been approximated as piecewise linear.\n"
+"This introduces error, typically of around 1%%."
+msgstr ""
+"Plocha vybraných stěn je:\n"
+"\n"
+" %s\n"
+"\n"
+"Křivky byly aproximovány jako po částech lineární.\n"
+"To přináší chybu, obvykle kolem 1%%."
+
+#: solvespace.cpp:906
+msgid ""
+"This group does not contain a correctly-formed 2d closed area. It is open, "
+"not coplanar, or self-intersecting."
+msgstr ""
+"Tato skupina neobsahuje správně vytvořenou uzavřenou 2D oblast. Je otevřená, "
+"není koplanární ani se neprotíná."
+
+#: solvespace.cpp:918
+#, c-format
+msgid ""
+"The area of the region sketched in this group is:\n"
+"\n"
+" %s\n"
+"\n"
+"Curves have been approximated as piecewise linear.\n"
+"This introduces error, typically of around 1%%."
+msgstr ""
+"Plocha regionu zakresleného v této skupině je:\n"
+"\n"
+" %s\n"
+"\n"
+"Křivky byly aproximovány jako po částech lineární.\n"
+"To přináší chybu, obvykle kolem 1%%."
+
+#: solvespace.cpp:938
+#, c-format
+msgid ""
+"The total length of the selected entities is:\n"
+"\n"
+" %s\n"
+"\n"
+"Curves have been approximated as piecewise linear.\n"
+"This introduces error, typically of around 1%%."
+msgstr ""
+"Celková délka vybraných entit je:\n"
+"\n"
+" %s\n"
+"\n"
+"Křivky byly aproximovány jako po částech lineární.\n"
+"To přináší chybu, obvykle kolem 1%%."
+
+#: solvespace.cpp:944
+msgid "Bad selection for perimeter; select line segments, arcs, and curves."
+msgstr "Chybný výběr obvodu; vyber úsečky, oblouky a křivky."
+
+#: solvespace.cpp:960
+msgid "Bad selection for trace; select a single point."
+msgstr "Chybný výběr pro trasování; vyber jeden bod."
+
+#: solvespace.cpp:987
+#, c-format
+msgid "Couldn't write to '%s'"
+msgstr "Nelze zapisovat do '%s'"
+
+#: solvespace.cpp:1017
+msgid "The mesh is self-intersecting (NOT okay, invalid)."
+msgstr "Síť se sama protíná (NENÍ v pořádku, je neplatná)."
+
+#: solvespace.cpp:1018
+msgid "The mesh is not self-intersecting (okay, valid)."
+msgstr "Síť se sama neprotíná (je v pořádku, je platná)."
+
+#: solvespace.cpp:1020
+msgid "The mesh has naked edges (NOT okay, invalid)."
+msgstr "Síť má obnažené hrany (NENÍ v pořádku, je neplatná)."
+
+#: solvespace.cpp:1021
+msgid "The mesh is watertight (okay, valid)."
+msgstr "Síť je vodotěsná (je v pořádku, je platná)."
+
+#: solvespace.cpp:1024
+#, c-format
+msgid ""
+"\n"
+"\n"
+"The model contains %d triangles, from %d surfaces."
+msgstr ""
+"\n"
+"\n"
+"Model obsahuje %d trojúhelníků z %d povrchů."
+
+#: solvespace.cpp:1028
+#, c-format
+msgid ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"Zero problematic edges, good.%s"
+msgstr ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"Žádné problematické hrany, v pořádku.%s"
+
+#: solvespace.cpp:1031
+#, c-format
+msgid ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"%d problematic edges, bad.%s"
+msgstr ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"Chyba, počet problematických hran: %d.%s"
+
+#: solvespace.cpp:1044
+#, c-format
+msgid ""
+"This is SolveSpace version %s.\n"
+"\n"
+"For more information, see http://solvespace.com/\n"
+"\n"
+"SolveSpace is free software: you are free to modify\n"
+"and/or redistribute it under the terms of the GNU\n"
+"General Public License (GPL) version 3 or later.\n"
+"\n"
+"There is NO WARRANTY, to the extent permitted by\n"
+"law. For details, visit http://gnu.org/licenses/\n"
+"\n"
+"© 2008-%d Jonathan Westhues and other authors.\n"
+msgstr ""
+"Toto je SolveSpace verze %s.\n"
+"\n"
+"Další informace naleznete na adrese http://solvespace.com/\n"
+"\n"
+"SolveSpace je svobodný software: můžete jej svobodně upravovat\n"
+"a/nebo jej šířit za podmínek GNU\n"
+"General Public License (GPL) verze 3 nebo novější.\n"
+"\n"
+"V rozsahu povoleném zákonem není poskytována ŽÁDNÁ ZÁRUKA.\n"
+"Podrobnosti najdete na adrese http://gnu.org/licenses/\n"
+"\n"
+"© 2008-%d Jonathan Westhues a další autoři.\n"
+
+#: style.cpp:185
+msgid ""
+"Can't assign style to an entity that's derived from another entity; try "
+"assigning a style to this entity's parent."
+msgstr ""
+"Nelze přiřadit styl entitě, která je odvozena od jiné entity; zkus "
+"přiřadit styl nadřazené entitě."
+
+#: style.cpp:735
+msgid "Style name cannot be empty"
+msgstr "Název stylu nemůže být prázdný"
+
+#: textscreens.cpp:791
+msgid "Can't repeat fewer than 1 time."
+msgstr "Nelze opakovat méně než 1krát."
+
+#: textscreens.cpp:795
+msgid "Can't repeat more than 999 times."
+msgstr "Nelze opakovat více než 999krát."
+
+#: textscreens.cpp:820
+msgid "Group name cannot be empty"
+msgstr "Název skupiny nemůže být prázdný"
+
+#: textscreens.cpp:872
+msgid "Opacity must be between zero and one."
+msgstr "Neprůhlednost musí být mezi nulou a jedničkou."
+
+#: textscreens.cpp:907
+msgid "Radius cannot be zero or negative."
+msgstr "Poloměr nemůže být nulový nebo záporný."
+
+#: toolbar.cpp:18
+msgid "Sketch line segment"
+msgstr "Náčrt úsečky"
+
+#: toolbar.cpp:20
+msgid "Sketch rectangle"
+msgstr "Náčrt obdélníku"
+
+#: toolbar.cpp:22
+msgid "Sketch circle"
+msgstr "Náčrt kružnice"
+
+#: toolbar.cpp:24
+msgid "Sketch arc of a circle"
+msgstr "Náčrt oblouku kružnice"
+
+#: toolbar.cpp:26
+msgid "Sketch curves from text in a TrueType font"
+msgstr "Náčrt křivek z textu v písmu TrueType"
+
+#: toolbar.cpp:28
+msgid "Sketch image from a file"
+msgstr "Náčrt obrázku ze souboru"
+
+#: toolbar.cpp:30
+msgid "Create tangent arc at selected point"
+msgstr "Vytvořit tečný oblouk ve vybraném bodě"
+
+#: toolbar.cpp:32
+msgid "Sketch cubic Bezier spline"
+msgstr "Náčrt kubického Bézierova splajnu"
+
+#: toolbar.cpp:34
+msgid "Sketch datum point"
+msgstr "Náčrt vztažného bodu"
+
+#: toolbar.cpp:36
+msgid "Toggle construction"
+msgstr "Přepnout konstrukci"
+
+#: toolbar.cpp:38
+msgid "Split lines / curves where they intersect"
+msgstr "Rozdělení úseček / křivek v místě jejich průsečíku"
+
+#: toolbar.cpp:42
+msgid "Constrain distance / diameter / length"
+msgstr "Omezení vzdálenosti / průměru / délky"
+
+#: toolbar.cpp:44
+msgid "Constrain angle"
+msgstr "Omezení úhlu"
+
+#: toolbar.cpp:46
+msgid "Constrain to be horizontal"
+msgstr "Omezení horizontály"
+
+#: toolbar.cpp:48
+msgid "Constrain to be vertical"
+msgstr "Omezení vertikály"
+
+#: toolbar.cpp:50
+msgid "Constrain to be parallel or tangent"
+msgstr "Omezení rovnoběžnosti nebo tečny"
+
+#: toolbar.cpp:52
+msgid "Constrain to be perpendicular"
+msgstr "Omezení kolmosti"
+
+#: toolbar.cpp:54
+msgid "Constrain point on line / curve / plane / point"
+msgstr "Omezení bodu na přímce / křivce / rovině / bodu"
+
+#: toolbar.cpp:56
+msgid "Constrain symmetric"
+msgstr "Omezení symetrie"
+
+#: toolbar.cpp:58
+msgid "Constrain equal length / radius / angle"
+msgstr "Omezení shodné délky / poloměru / úhlu"
+
+#: toolbar.cpp:60
+msgid "Constrain normals in same orientation"
+msgstr "Omezení normál se stejnou orientací"
+
+#: toolbar.cpp:62
+msgid "Other supplementary angle"
+msgstr "Další doplňkový úhel"
+
+#: toolbar.cpp:64
+msgid "Toggle reference dimension"
+msgstr "Přepnout referenční rozměr"
+
+#: toolbar.cpp:68
+msgid "New group extruding active sketch"
+msgstr "Nová skupina extruzí aktivního náčrtu"
+
+#: toolbar.cpp:70
+msgid "New group rotating active sketch"
+msgstr "Nová skupina plnou rotací aktivního náčrtu"
+
+#: toolbar.cpp:72
+msgid "New group helix from active sketch"
+msgstr "Nová skupina šroubovicí aktivního náčrtu"
+
+#: toolbar.cpp:74
+msgid "New group revolve active sketch"
+msgstr "Nová skupina volnou rotací aktivního náčrtu"
+
+#: toolbar.cpp:76
+msgid "New group step and repeat rotating"
+msgstr "Nová skupina krokovým otočením"
+
+#: toolbar.cpp:78
+msgid "New group step and repeat translating"
+msgstr "Nová skupina krokovým posunem"
+
+#: toolbar.cpp:80
+msgid "New group in new workplane (thru given entities)"
+msgstr "Nová skupina v pracovní rovině (přes dané entity)"
+
+#: toolbar.cpp:82
+msgid "New group in 3d"
+msgstr "Nová skupina ve 3D"
+
+#: toolbar.cpp:84
+msgid "New group linking / assembling file"
+msgstr "Nová skupina odkazem / sestavením souboru"
+
+#: toolbar.cpp:88
+msgid "Nearest isometric view"
+msgstr "Nejbližší isometrický pohled"
+
+#: toolbar.cpp:90
+msgid "Align view to active workplane"
+msgstr "Zarovnat pohled na aktivní pracovní rovinu"
+
+#: util.cpp:165
+msgctxt "title"
+msgid "Error"
+msgstr "Chyba"
+
+#: util.cpp:165
+msgctxt "title"
+msgid "Message"
+msgstr "Zpráva"
+
+#: util.cpp:170
+msgctxt "button"
+msgid "&OK"
+msgstr "&OK"
+
+#: view.cpp:127
+msgid "Scale cannot be zero or negative."
+msgstr "Měřítko nemůže být nulové nebo záporné."
+
+#: view.cpp:139 view.cpp:148
+msgid "Bad format: specify x, y, z"
+msgstr "Chybný formát: zadej x, y, z"
+
+#~ msgid "&Mirror"
+#~ msgstr "&Zrcadlit"
+
+#~ msgctxt "group-name"
+#~ msgid "mirror"
+#~ msgstr "zrcadlit"
+
+#~ msgid ""
+#~ "Bad selection for length ratio constraint. This constraint can apply to:\n"
+#~ "\n"
+#~ " * two line segments\n"
+#~ msgstr ""
+#~ "Chybný výběr pro omezení poměru délky. Toto omezení lze použít pro:\n"
+#~ "\n"
+#~ " * dvě úsečky\n"
+
+#~ msgid ""
+#~ "Bad selection for length difference constraint. This constraint can apply "
+#~ "to:\n"
+#~ "\n"
+#~ " * two line segments\n"
+#~ msgstr ""
+#~ "Chybný výběr pro omezení rozdílu délek. Toto omezení lze použít"
+#~ "pro:\n"
+#~ "\n"
+#~ " * dvě úsečky\n"
+
+#~ msgid "Length Ra&tio"
+#~ msgstr "Poměr &délky"
+
+#~ msgid "Length Diff&erence"
+#~ msgstr "Rozdíl dél&ek"
+
+#~ msgid ""
+#~ "Bad selection for new sketch in workplane. This group can be created "
+#~ "with:\n"
+#~ "\n"
+#~ " * a point (through the point, orthogonal to coordinate axes)\n"
+#~ " * a point and two line segments (through the point, parallel to the "
+#~ "lines)\n"
+#~ " * a workplane (copy of the workplane)\n"
+#~ msgstr ""
+#~ "Chybný výběr nového náčrtu v pracovní rovině. Tuto skupinu lze vytvořit "
+#~ "pomocí:\n"
+#~ "\n"
+#~ " * bodu (procházejícího bodem kolmým na souřadnicové osy)\n"
+#~ " * bodu a dvěma úsečkami (procházejícího bodem rovnoběžným "
+#~ "s úsečkami)\n"
+#~ " * pracovní roviny (kopie pracovní roviny)\n"
+
+#~ msgctxt "file-type"
+#~ msgid "Q3D Object file"
+#~ msgstr "Q3D Objektový soubor"
diff --git a/res/locales/de_DE.po b/res/locales/de_DE.po
index 9791a538..11d77fac 100644
--- a/res/locales/de_DE.po
+++ b/res/locales/de_DE.po
@@ -7,15 +7,15 @@ msgstr ""
"Project-Id-Version: SolveSpace 3.0\n"
"Report-Msgid-Bugs-To: whitequark@whitequark.org\n"
"POT-Creation-Date: 2022-02-01 16:24+0200\n"
-"PO-Revision-Date: 2018-07-19 06:55+0000\n"
+"PO-Revision-Date: 2022-04-30 16:44+0200\n"
"Last-Translator: Reini Urban \n"
"Language-Team: none\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Zanata 4.5.0\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"X-Generator: Poedit 2.4.2\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: clipboard.cpp:309
msgid ""
@@ -26,7 +26,7 @@ msgstr ""
"Ausschneiden, Einfügen und Kopieren sind nur in einer Arbeitsebene "
"zulässig.\n"
"\n"
-"Aktivieren Sie eine mit Skizze -> In Arbeitsebene"
+"Aktivieren Sie eine mit \"Skizze -> In Arbeitsebene\"."
#: clipboard.cpp:326
msgid "Clipboard is empty; nothing to paste."
@@ -172,12 +172,12 @@ msgstr "Längenverhältnis"
#: constraint.cpp:25
msgctxt "constr-name"
msgid "arc-arc-length-ratio"
-msgstr ""
+msgstr "Bogen-Bogen-Längenverhältnis"
#: constraint.cpp:26
msgctxt "constr-name"
msgid "arc-line-length-ratio"
-msgstr ""
+msgstr "Bogen-Linien-Längenverhältnis"
#: constraint.cpp:27
msgctxt "constr-name"
@@ -187,12 +187,12 @@ msgstr "Längendifferenz"
#: constraint.cpp:28
msgctxt "constr-name"
msgid "arc-arc-len-difference"
-msgstr ""
+msgstr "Bogen-Bogen-Längendifferenz"
#: constraint.cpp:29
msgctxt "constr-name"
msgid "arc-line-len-difference"
-msgstr ""
+msgstr "Bogen-Linien-Längendifferenz"
#: constraint.cpp:30
msgctxt "constr-name"
@@ -306,7 +306,7 @@ msgid ""
msgstr ""
"Die Bogentangente und das Liniensegment müssen einen gemeinsamen Endpunkt "
"haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die "
-"Tangente einschränken. -> Sc"
+"Tangente einschränken."
#: constraint.cpp:163
msgid ""
@@ -315,7 +315,7 @@ msgid ""
msgstr ""
"Die Kurventangente und das Liniensegment müssen einen gemeinsamen Endpunkt "
"haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die "
-"Tangente einschränken. -> Sc"
+"Tangente einschränken."
#: constraint.cpp:189
msgid ""
@@ -323,7 +323,7 @@ msgid ""
"before constraining tangent."
msgstr ""
"Die Kurven müssen einen gemeinsamen Endpunkt haben. Schränken Sie mit "
-"\"Einschränkung / Auf Punkt\" ein, bevor Sie die Tangente einschränken. -> Sc"
+"\"Einschränkung / Auf Punkt\" ein, bevor Sie die Tangente einschränken."
#: constraint.cpp:238
msgid ""
@@ -408,6 +408,12 @@ msgid ""
" * two arcs\n"
" * one arc and one line segment\n"
msgstr ""
+"Ungültige Auswahl für Einschränkung \"Längenverhältnis\". Diese "
+"Einschränkung ist anwendbar auf:\n"
+"\n"
+" * zwei Liniensegmente\n"
+" * zwei Bögen\n"
+" * einen Bogen und ein Liniensegment\n"
#: constraint.cpp:441
msgid ""
@@ -418,6 +424,12 @@ msgid ""
" * two arcs\n"
" * one arc and one line segment\n"
msgstr ""
+"Ungültige Auswahl für Einschränkung \"Längendifferenz\". Diese Einschränkung "
+"ist anwendbar auf:\n"
+"\n"
+" * zwei Liniensegmente\n"
+" * zwei Bögen\n"
+" * einen Bogen und ein Liniensegment\n"
#: constraint.cpp:472
msgid ""
@@ -584,7 +596,7 @@ msgid ""
"2d View to export bare lines and curves."
msgstr ""
"Kein Festkörper vorhanden; zeichnen Sie eines mit Extrusionen und Drehungen, "
-"oder exportieren Sie bloße Linien und Kurven mit \"2D-Ansicht exportieren\""
+"oder exportieren Sie bloße Linien und Kurven mit \"2D-Ansicht exportieren\"."
#: export.cpp:61
msgid ""
@@ -699,7 +711,7 @@ msgstr "&Neu"
#: graphicswin.cpp:43
msgid "&Open..."
-msgstr "&Öffnen"
+msgstr "&Öffnen..."
#: graphicswin.cpp:44
msgid "Open &Recent"
@@ -727,7 +739,7 @@ msgstr "Exportiere 2D-Auswahl…"
#: graphicswin.cpp:51
msgid "Export 3d &Wireframe..."
-msgstr "Exportiere 3D-Drahtgittermodell"
+msgstr "Exportiere 3D-Drahtgittermodell..."
#: graphicswin.cpp:52
msgid "Export Triangle &Mesh..."
@@ -859,7 +871,7 @@ msgstr "Perspektivische Projektion"
#: graphicswin.cpp:97
msgid "Show E&xploded View"
-msgstr ""
+msgstr "Zeige e&xplodierte Ansicht"
#: graphicswin.cpp:98
msgid "Dimension &Units"
@@ -879,7 +891,7 @@ msgstr "Maße in Zoll"
#: graphicswin.cpp:102
msgid "Dimensions in &Feet and Inches"
-msgstr ""
+msgstr "Maße in &Fuß und Inch"
#: graphicswin.cpp:104
msgid "Show &Toolbar"
@@ -931,7 +943,7 @@ msgstr "D&rehen"
#: graphicswin.cpp:121
msgid "Link / Assemble..."
-msgstr "Verknüpfen / Zusammensetzen"
+msgstr "Verknüpfen / Zusammensetzen..."
#: graphicswin.cpp:122
msgid "Link Recent"
@@ -1047,11 +1059,11 @@ msgstr "Gleicher Abstand / Radius / Winkel"
#: graphicswin.cpp:158
msgid "Length / Arc Ra&tio"
-msgstr ""
+msgstr "Länge / Bogen Verhäl&tnis"
#: graphicswin.cpp:159
msgid "Length / Arc Diff&erence"
-msgstr ""
+msgstr "Länge / Bogen Diff&erenz"
#: graphicswin.cpp:160
msgid "At &Midpoint"
@@ -1119,7 +1131,7 @@ msgstr "Punkt nachzeichnen"
#: graphicswin.cpp:180
msgid "&Stop Tracing..."
-msgstr "Nachzeichnen beenden"
+msgstr "Nachzeichnen beenden..."
#: graphicswin.cpp:181
msgid "Step &Dimension..."
@@ -1139,7 +1151,7 @@ msgstr "&Website / Anleitung"
#: graphicswin.cpp:186
msgid "&Go to GitHub commit"
-msgstr ""
+msgstr "&Gehe zu GitHub commit"
#: graphicswin.cpp:188
msgid "&About"
@@ -1300,6 +1312,12 @@ msgid ""
" * a point and a normal (through the point, orthogonal to the normal)\n"
" * a workplane (copy of the workplane)\n"
msgstr ""
+"Ungültige Auswahl für neue Skizze in der Arbeitsebene. Diese Gruppe kann "
+"erstellt werden mit:\n"
+"\n"
+" * einem Punkt (durch den Punkt, orthogonal zur Koordinatenachse)\n"
+" * einem Punkt und zwei Linienabschnitten (durch den Punkt, parallel zu "
+"den Linien)\n"
#: group.cpp:166
msgid ""
@@ -1307,7 +1325,7 @@ msgid ""
"will be extruded normal to the workplane."
msgstr ""
"Aktivieren Sie vor der Extrusion eine Arbeitsebene (mit Skizze -> In "
-"Arbeitsebene). Die Skizze wird senkrecht zur Arbeitsebene extrudiert"
+"Arbeitsebene). Die Skizze wird senkrecht zur Arbeitsebene extrudiert."
#: group.cpp:175
msgctxt "group-name"
@@ -1434,7 +1452,7 @@ msgstr "Kante mit Länge Null!"
#: importmesh.cpp:136
msgid "Text-formated STL files are not currently supported"
-msgstr ""
+msgstr "Text-formatierte STL Dateien werden aktuell nicht unterstützt"
#: modify.cpp:252
msgid "Must be sketching in workplane to create tangent arc."
@@ -1447,7 +1465,7 @@ msgid ""
msgstr ""
"Um eine Bogentangente zu erstellen, wählen Sie einen Punkt, in dem sich zwei "
"nicht-Konstruktionslinien oder -kreise in dieser Gruppe und Arbeitsebene "
-"treffen. "
+"treffen."
#: modify.cpp:386
msgid ""
@@ -1646,7 +1664,7 @@ msgstr "SolveSpace-Modelle"
#: platform/gui.cpp:89
msgctxt "file-type"
msgid "ALL"
-msgstr ""
+msgstr "ALLE"
#: platform/gui.cpp:91
msgctxt "file-type"
@@ -1656,7 +1674,7 @@ msgstr "IDF Leiterplatte"
#: platform/gui.cpp:92
msgctxt "file-type"
msgid "STL triangle mesh"
-msgstr ""
+msgstr "STL-Dreiecks-Netz"
#: platform/gui.cpp:96
msgctxt "file-type"
@@ -2072,7 +2090,7 @@ msgstr ""
#: style.cpp:735
msgid "Style name cannot be empty"
-msgstr "Name des Linientyps kann nicht leer sein."
+msgstr "Name des Linientyps kann nicht leer sein"
#: textscreens.cpp:791
msgid "Can't repeat fewer than 1 time."
@@ -2084,7 +2102,7 @@ msgstr "Nicht mehr als 999 Wiederholungen möglich."
#: textscreens.cpp:820
msgid "Group name cannot be empty"
-msgstr "Der Name der Gruppe darf nicht leer sein."
+msgstr "Der Name der Gruppe darf nicht leer sein"
#: textscreens.cpp:872
msgid "Opacity must be between zero and one."
diff --git a/res/locales/fr_FR.po b/res/locales/fr_FR.po
index ec6d3bac..073d4ebd 100644
--- a/res/locales/fr_FR.po
+++ b/res/locales/fr_FR.po
@@ -7,14 +7,14 @@ msgstr ""
"Project-Id-Version: SolveSpace 3.0\n"
"Report-Msgid-Bugs-To: whitequark@whitequark.org\n"
"POT-Creation-Date: 2022-02-01 16:24+0200\n"
-"PO-Revision-Date: 2018-07-14 06:12+0000\n"
+"PO-Revision-Date: 2022-10-15 17:32+0200\n"
"Last-Translator: whitequark \n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Zanata 4.4.5\n"
+"X-Generator: Poedit 2.4.2\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: clipboard.cpp:309
@@ -23,13 +23,13 @@ msgid ""
"\n"
"Activate one with Sketch -> In Workplane."
msgstr ""
-"Couper, coller et copier uniquement dans un plan de travail.\n"
+"Couper, coller et copier fonctionnent uniquement dans un plan de travail.\n"
"\n"
-"Activez un plan avec \"Dessin -> Dans plan de travail\"."
+"Activez un plan avec « Dessin -> Dans le plan de travail »."
#: clipboard.cpp:326
msgid "Clipboard is empty; nothing to paste."
-msgstr "Presse papier vide; rien à coller."
+msgstr "Presse papier vide ; rien à coller."
#: clipboard.cpp:373
msgid "Number of copies to paste must be at least one."
@@ -57,7 +57,7 @@ msgstr ""
#: clipboard.cpp:457
msgid "Too many items to paste; split this into smaller pastes."
-msgstr "Trop d'éléments à coller; Divisez-les en plus petits groupes."
+msgstr "Trop d'éléments à coller ; divisez-les en plus petits groupes."
#: clipboard.cpp:462
msgid "No workplane active."
@@ -65,11 +65,11 @@ msgstr "Pas d'espace de travail actif."
#: confscreen.cpp:381
msgid "Bad format: specify coordinates as x, y, z"
-msgstr "Mauvais format: spécifiez les coordonnées comme x, y, z"
+msgstr "Mauvais format : spécifiez les coordonnées comme x, y, z"
#: confscreen.cpp:391 style.cpp:729 textscreens.cpp:864
msgid "Bad format: specify color as r, g, b"
-msgstr "Mauvais format; spécifiez la couleur comme r, v, b"
+msgstr "Mauvais format ; spécifiez la couleur comme r, v, b"
#: confscreen.cpp:417
msgid ""
@@ -77,30 +77,30 @@ msgid ""
"Perspective Projection."
msgstr ""
"Le facteur de perspective n'aura aucun effet tant que vous n'aurez pas "
-"activé \"Affichage -> Utiliser la projection de perspective\"."
+"activé « Affichage -> Utiliser la Vue en perspective »."
#: confscreen.cpp:435 confscreen.cpp:445
#, c-format
msgid "Specify between 0 and %d digits after the decimal."
-msgstr ""
+msgstr "Spécifier entre 0 et %d chiffres après la virgule."
#: confscreen.cpp:457
msgid "Export scale must not be zero!"
-msgstr "L'échelle d'export ne doit pas être zéro!"
+msgstr "L'échelle d'export ne doit pas être zéro !"
#: confscreen.cpp:469
msgid "Cutter radius offset must not be negative!"
-msgstr "Le décalage du rayon de coupe ne doit pas être négatif!"
+msgstr "Le décalage du rayon de coupe ne doit pas être négatif !"
#: confscreen.cpp:528
msgid "Bad value: autosave interval should be positive"
msgstr ""
-"Mauvaise valeur: l'intervalle d'enregistrement automatique devrait être "
+"Mauvaise valeur : l'intervalle d'enregistrement automatique devrait être "
"positif"
#: confscreen.cpp:531
msgid "Bad format: specify interval in integral minutes"
-msgstr "Mauvais format: spécifiez un nombre entier de minutes"
+msgstr "Mauvais format : spécifiez un nombre entier de minutes"
#: constraint.cpp:12
msgctxt "constr-name"
@@ -170,12 +170,12 @@ msgstr "longueur-ratio"
#: constraint.cpp:25
msgctxt "constr-name"
msgid "arc-arc-length-ratio"
-msgstr ""
+msgstr "arc-arc-longueur-ratio"
#: constraint.cpp:26
msgctxt "constr-name"
msgid "arc-line-length-ratio"
-msgstr ""
+msgstr "arc-ligne-longueur-ratio"
#: constraint.cpp:27
msgctxt "constr-name"
@@ -185,12 +185,12 @@ msgstr "longueur-difference"
#: constraint.cpp:28
msgctxt "constr-name"
msgid "arc-arc-len-difference"
-msgstr ""
+msgstr "arc-arc-longueur-différence"
#: constraint.cpp:29
msgctxt "constr-name"
msgid "arc-line-len-difference"
-msgstr ""
+msgstr "arc-ligne-longueur-différence"
#: constraint.cpp:30
msgctxt "constr-name"
@@ -302,26 +302,26 @@ msgid ""
"The tangent arc and line segment must share an endpoint. Constrain them with "
"Constrain -> On Point before constraining tangent."
msgstr ""
-"L'arc tangent et le segment de ligne doivent partager un point final. "
-"Contraignez-les avec \"Contrainte -> Sur point avant de contraindre la "
-"tangente\"."
+"L'arc tangent et le segment de ligne doivent partager une extrémité. "
+"Contraignez-les avec « Contraintes -> Sur Point » avant de contraindre la "
+"tangente."
#: constraint.cpp:163
msgid ""
"The tangent cubic and line segment must share an endpoint. Constrain them "
"with Constrain -> On Point before constraining tangent."
msgstr ""
-"La tangente cubique et le segment de ligne doivent partager un point final. "
-"Contraignez-les avec \"Contrainte -> Sur point avant de contraindre la "
-"tangente\"."
+"La tangente cubique et le segment de ligne doivent partager une extrémité. "
+"Contraignez-les avec « Contraintes -> Sur Point » avant de contraindre la "
+"tangente."
#: constraint.cpp:189
msgid ""
"The curves must share an endpoint. Constrain them with Constrain -> On Point "
"before constraining tangent."
msgstr ""
-"Les courbes doivent partager un point final. Contraignez-les avec "
-"\"Contrainte -> Sur point avant de contraindre la tangente\"."
+"Les courbes doivent partager une extrémité. Contraignez-les avec "
+"« Contraintes -> Sur Point » avant de contraindre la tangente."
#: constraint.cpp:238
msgid ""
@@ -337,15 +337,15 @@ msgid ""
" * a circle or an arc (diameter)\n"
msgstr ""
"Mauvaise sélection pour la contrainte distance / diamètre. Cette contrainte "
-"peut s'appliquer à:\n"
+"peut s'appliquer à :\n"
"\n"
-" * Deux points (distance entre points)\n"
-" * Un segment de ligne (longueur)\n"
-" * Deux points et un segment de ligne ou normal (distance projetée)\n"
-" * Un plan de travail et un point (distance minimale)\n"
-" * Un segment de ligne et un point (distance minimale)\n"
-" * Une face plane et un point (distance minimale)\n"
-" * Un cercle ou un arc (diamètre)\n"
+" – Deux points (distance entre points)\n"
+" – Un segment de ligne (longueur)\n"
+" – Deux points et un segment de ligne ou normal (distance projetée)\n"
+" – Un plan de travail et un point (distance minimale)\n"
+" – Un segment de ligne et un point (distance minimale)\n"
+" – Une face plane et un point (distance minimale)\n"
+" – Un cercle ou un arc (diamètre)\n"
#: constraint.cpp:291
msgid ""
@@ -359,13 +359,13 @@ msgid ""
" * a point and a plane face (point on face)\n"
msgstr ""
"Mauvaise sélection pour la contrainte point / courbe / plan. Cette "
-"contrainte peut s'appliquer à:\n"
+"contrainte peut s'appliquer à :\n"
"\n"
-" * Deux points (points coïncidents)\n"
-" * Un point et un plan de travail (point dans le plan)\n"
-" * Un point et un segment de ligne (point en ligne)\n"
-" * Un point et un cercle ou un arc (point sur courbe)\n"
-" * Un point et une face plane (point sur une face)\n"
+" – Deux points (points coïncidents)\n"
+" – Un point et un plan de travail (point dans le plan)\n"
+" – Un point et un segment de ligne (point en ligne)\n"
+" – Un point et un cercle ou un arc (point sur courbe)\n"
+" – Un point et une face plane (point sur une face)\n"
#: constraint.cpp:353
msgid ""
@@ -383,19 +383,19 @@ msgid ""
" * a line segment and an arc (line segment length equals arc length)\n"
msgstr ""
"Mauvaise sélection pour une contrainte de longueur / rayon égale. Cette "
-"contrainte peut s'appliquer à:\n"
+"contrainte peut s'appliquer à :\n"
"\n"
-" * Deux segments de ligne (longueur égale)\n"
-" * Deux segments de ligne et deux points (distances point-ligne égales)\n"
-" * Un segment de ligne et deux points (distances point-ligne égales)\n"
-" * Un segment de ligne ou un segment de ligne et point (distance point-"
+" – Deux segments de ligne (longueur égale)\n"
+" – Deux segments de ligne et deux points (distances point-ligne égales)\n"
+" – Un segment de ligne et deux points (distances point-ligne égales)\n"
+" – Un segment de ligne ou un segment de ligne et point (distance point-"
"ligne de longueur égale)\n"
-" * Quatre segments de ligne ou des normales (angle entre A, B et C, D "
+" – Quatre segments de ligne ou des normales (angle entre A, B et C, D "
"égaux)\n"
-" * Trois segments de ligne ou des normales (angle entre A, B et B, C "
+" – Trois segments de ligne ou des normales (angle entre A, B et B, C "
"égaux)\n"
-" * Deux cercles ou arcs (rayon égaux)\n"
-" * Un segment de ligne et un arc (la longueur de segment de ligne est "
+" – Deux cercles ou arcs (rayon égaux)\n"
+" – Un segment de ligne et un arc (la longueur de segment de ligne est "
"égale à la longueur d'arc)\n"
#: constraint.cpp:407
@@ -406,6 +406,12 @@ msgid ""
" * two arcs\n"
" * one arc and one line segment\n"
msgstr ""
+"Mauvaise sélection pour une contrainte de ratio de longueur. Cette contrainte peut "
+"s'appliquer à :\n"
+"\n"
+" – Deux segments de ligne\n"
+" – Deux arcs\n"
+" – Un arc et un segment de ligne\n"
#: constraint.cpp:441
msgid ""
@@ -416,6 +422,12 @@ msgid ""
" * two arcs\n"
" * one arc and one line segment\n"
msgstr ""
+"Mauvaise sélection pour une contrainte de différence de longueur. Cette contrainte "
+"peut s'appliquer à :\n"
+"\n"
+" – Deux segments de ligne\n"
+" – Deux arcs\n"
+" – Un arc et un segment de ligne\n"
#: constraint.cpp:472
msgid ""
@@ -425,10 +437,10 @@ msgid ""
" * a line segment and a workplane (line's midpoint on plane)\n"
msgstr ""
"Mauvaise sélection pour une contrainte de point médian. Cette contrainte "
-"peut s'appliquer à:\n"
+"peut s'appliquer à :\n"
"\n"
-" * Un segment de ligne et un point (point au milieu)\n"
-" * Un segment de ligne et un plan de travail (point médian dans le plan)\n"
+" – Un segment de ligne et un point (point au milieu)\n"
+" – Un segment de ligne et un plan de travail (point médian dans le plan)\n"
#: constraint.cpp:530
msgid ""
@@ -442,13 +454,13 @@ msgid ""
"workplane)\n"
msgstr ""
"Mauvaise sélection pour la contrainte symétrique. Cette contrainte peut "
-"s'appliquer à:\n"
+"s'appliquer à :\n"
"\n"
-" * Deux points ou un segment de ligne (symétrique à l'axe des coordonnées "
+" – Deux points ou un segment de ligne (symétrique à l'axe des coordonnées "
"du plan de travail)\n"
-" * Segment de ligne, et deux points ou un segment de ligne (symétrique "
+" – Segment de ligne, et deux points ou un segment de ligne (symétrique "
"sur le segment de ligne)\n"
-" * Plan de travail, et deux points ou un segment de ligne (symétrique au "
+" – Plan de travail, et deux points ou un segment de ligne (symétrique au "
"plan de travail)\n"
#: constraint.cpp:545
@@ -464,7 +476,7 @@ msgid ""
"Activate a workplane (with Sketch -> In Workplane) before applying a "
"horizontal or vertical constraint."
msgstr ""
-"Activez un plan de travail (avec Dessin -> Dans plan de travail) avant "
+"Activez un plan de travail (avec Dessin -> Dans le plan de travail) avant "
"d'appliquer une contrainte horizontale ou verticale."
#: constraint.cpp:592
@@ -476,10 +488,10 @@ msgid ""
" * a line segment\n"
msgstr ""
"Mauvaise sélection pour la contrainte horizontale / verticale. Cette "
-"contrainte peut s'appliquer à:\n"
+"contrainte peut s'appliquer à :\n"
"\n"
-" * deux points\n"
-" * Un segment de ligne\n"
+" – deux points\n"
+" – Un segment de ligne\n"
#: constraint.cpp:613
msgid ""
@@ -489,9 +501,9 @@ msgid ""
" * two normals\n"
msgstr ""
"Mauvaise sélection pour la même contrainte d'orientation. Cette contrainte "
-"peut s'appliquer à:\n"
+"peut s'appliquer à :\n"
"\n"
-" * Deux normales\n"
+" – Deux normales\n"
#: constraint.cpp:663
msgid "Must select an angle constraint."
@@ -510,15 +522,15 @@ msgid ""
" * two normals\n"
msgstr ""
"Mauvaise sélection pour une contrainte d'angle. Cette contrainte peut "
-"s'appliquer à:\n"
+"s'appliquer à :\n"
"\n"
-" * Deux segments de ligne\n"
-" * Un segment de ligne et une normale\n"
-" * Deux normales\n"
+" – Deux segments de ligne\n"
+" – Un segment de ligne et une normale\n"
+" – Deux normales\n"
#: constraint.cpp:754
msgid "Curve-curve tangency must apply in workplane."
-msgstr "Courbe-Courbe tangence doit s'appliquer dans le plan de travail."
+msgstr "La tangence courbe-courbe doit s'appliquer dans le plan de travail."
#: constraint.cpp:766
msgid ""
@@ -531,13 +543,13 @@ msgid ""
" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n"
msgstr ""
"Mauvaise sélection pour la contrainte parallèle / tangente. Cette contrainte "
-"peut s'appliquer à:\n"
+"peut s'appliquer à :\n"
"\n"
-" * Deux segments de ligne (parallèles)\n"
-" * Un segment de ligne et un parallèle (parallèle)\n"
-" * Deux normales (parallèles)\n"
-" * Deux segments de ligne, des arcs ou des Béziers, qui partagent un "
-"point final (tangent)\n"
+" – Deux segments de ligne (parallèles)\n"
+" – Un segment de ligne et un parallèle (parallèle)\n"
+" – Deux normales (parallèles)\n"
+" – Deux segments de ligne, des arcs ou des Béziers, qui partagent une "
+"extrémité (tangente)\n"
#: constraint.cpp:784
msgid ""
@@ -548,11 +560,11 @@ msgid ""
" * two normals\n"
msgstr ""
"Mauvaise sélection pour une contrainte perpendiculaire. Cette contrainte "
-"peut s'appliquer à:\n"
+"peut s'appliquer à :\n"
"\n"
-" * Deux segments de ligne\n"
-" * Un segment de ligne et une normale\n"
-" * Deux normales\n"
+" – Deux segments de ligne\n"
+" – Un segment de ligne et une normale\n"
+" – Deux normales\n"
#: constraint.cpp:799
msgid ""
@@ -562,13 +574,13 @@ msgid ""
" * a point\n"
msgstr ""
"Mauvaise sélection pour le point de verrouillage où la contrainte déplacé. "
-"Cette contrainte peut s'appliquer à:\n"
+"Cette contrainte peut s'appliquer à :\n"
"\n"
-" * un point\n"
+" – un point\n"
#: constraint.cpp:813 mouse.cpp:1158
msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT"
-msgstr "NOUVEAU COMMENTAIRE - DOUBLE-CLIQUE POUR EDITER"
+msgstr "NOUVEAU COMMENTAIRE — DOUBLE-CLIQUEZ POUR EDITER"
#: constraint.cpp:818
msgid "click center of comment text"
@@ -579,9 +591,8 @@ msgid ""
"No solid model present; draw one with extrudes and revolves, or use Export "
"2d View to export bare lines and curves."
msgstr ""
-"Aucun modèle solide présent; Dessinez-en un avec une extrusion et "
-"révolution, ou utilisez \"Exporter vue 2d\" pour exporter les lignes et les "
-"courbes dépouillées."
+"Aucun modèle solide présent ; dessinez-en un avec une extrusion et révolution, ou "
+"utilisez « Exporter vue 2d » pour exporter les lignes et les courbes dépouillées."
#: export.cpp:61
msgid ""
@@ -592,17 +603,17 @@ msgid ""
" * a point and two line segments (plane through point and parallel to "
"lines)\n"
msgstr ""
-"Mauvaise sélection pour la section export. Sélectionnez:\n"
+"Mauvaise sélection pour la section export. Sélectionnez :\n"
"\n"
-" * Rien, avec un plan de travail actif (plan de travail est un plan de "
+" – Rien, avec un plan de travail actif (plan de travail est un plan de "
"section)\n"
-" * Une face (plan de coupe au-travers d'une face)\n"
-" * Un point et deux segments de ligne (plan au-travers d'un point et "
+" – Une face (plan de coupe au-travers d'une face)\n"
+" – Un point et deux segments de ligne (plan au-travers d'un point et "
"parallèle aux lignes)\n"
#: export.cpp:818
msgid "Active group mesh is empty; nothing to export."
-msgstr "Le maillage du groupe actif est vide; Rien à exporter."
+msgstr "Le maillage du groupe actif est vide ; Rien à exporter."
#: exportvector.cpp:336
msgid "freehand lines were replaced with continuous lines"
@@ -617,14 +628,14 @@ msgid ""
"Some aspects of the drawing have no DXF equivalent and were not exported:\n"
msgstr ""
"Certains aspects du dessin n'ont pas d'équivalent DXF et n'ont pas été "
-"exportés:\n"
+"exportés :\n"
#: exportvector.cpp:838
msgid ""
"PDF page size exceeds 200 by 200 inches; many viewers may reject this file."
msgstr ""
-"La taille de la page PDF dépasse 200 par 200 pouces; De nombreux lecteurs "
-"peuvent rejeter ce fichier."
+"La taille de la page PDF dépasse 200 par 200 pouces ; de nombreux lecteurs "
+"risquent rejeter ce fichier."
#: file.cpp:44 group.cpp:91
msgctxt "group-name"
@@ -638,15 +649,15 @@ msgstr "#références"
#: file.cpp:550
msgid "The file is empty. It may be corrupt."
-msgstr ""
+msgstr "Le fichier est vide. Il est possible qu'il soit corrompu."
#: file.cpp:555
msgid ""
"Unrecognized data in file. This file may be corrupt, or from a newer version "
"of the program."
msgstr ""
-"Données non reconnues dans le fichier. Ce fichier peut être corrompu ou "
-"depuis une version plus récente du programme."
+"Données non reconnues dans le fichier. Ce fichier peut être corrompu, ou venir "
+"d'une version plus récente du programme."
#: file.cpp:867
msgctxt "title"
@@ -657,7 +668,7 @@ msgstr "Fichier manquant"
#, c-format
msgctxt "dialog"
msgid "The linked file “%s” is not present."
-msgstr ""
+msgstr "Le fichier lié « %s » n'est pas présent."
#: file.cpp:870
msgctxt "dialog"
@@ -667,21 +678,25 @@ msgid ""
"If you decline, any geometry that depends on the missing file will be "
"permanently removed."
msgstr ""
+"Voulez-vous le chercher manuellement ?\n"
+"\n"
+"Sinon, toute géométrie qui dépend du fichier manquant sera définitivement "
+"supprimée."
#: file.cpp:873
msgctxt "button"
msgid "&Yes"
-msgstr ""
+msgstr "&Oui"
#: file.cpp:875
msgctxt "button"
msgid "&No"
-msgstr ""
+msgstr "&Non"
#: file.cpp:877 solvespace.cpp:611
msgctxt "button"
msgid "&Cancel"
-msgstr ""
+msgstr "&Annuler"
#: graphicswin.cpp:41
msgid "&File"
@@ -761,7 +776,7 @@ msgstr "Accrocher la sélection à la &Grille"
#: graphicswin.cpp:66
msgid "Rotate Imported &90°"
-msgstr "Rotation importation &90°"
+msgstr "Tourner l'import de &90°"
#: graphicswin.cpp:68
msgid "Cu&t"
@@ -797,7 +812,7 @@ msgstr "&Désélectionner Tout"
#: graphicswin.cpp:78
msgid "&Line Styles..."
-msgstr "&Ligne styles..."
+msgstr "&Styles de Ligne..."
#: graphicswin.cpp:79
msgid "&View Projection..."
@@ -845,15 +860,15 @@ msgstr "Afficher la &grille d'accrochage"
#: graphicswin.cpp:95
msgid "Darken Inactive Solids"
-msgstr ""
+msgstr "Noircir Solides Inactifs"
#: graphicswin.cpp:96
msgid "Use &Perspective Projection"
-msgstr "Utiliser la vue en &Perspective"
+msgstr "Utiliser la Vue en &Perspective"
#: graphicswin.cpp:97
msgid "Show E&xploded View"
-msgstr ""
+msgstr "Afficher Vue Éclatée"
#: graphicswin.cpp:98
msgid "Dimension &Units"
@@ -873,7 +888,7 @@ msgstr "Dimensions en &Pouces"
#: graphicswin.cpp:102
msgid "Dimensions in &Feet and Inches"
-msgstr ""
+msgstr "Dimensions en &Pieds et Pouces"
#: graphicswin.cpp:104
msgid "Show &Toolbar"
@@ -901,11 +916,11 @@ msgstr "Dessin dans un nouveau &Plan de travail"
#: graphicswin.cpp:113
msgid "Step &Translating"
-msgstr "Espacement &Linéaire"
+msgstr "Répéter par &Translation"
#: graphicswin.cpp:114
msgid "Step &Rotating"
-msgstr "Espacement &Circulaire"
+msgstr "Répéter par &Rotation"
#: graphicswin.cpp:116
msgid "E&xtrude"
@@ -913,11 +928,11 @@ msgstr "E&xtruder"
#: graphicswin.cpp:117
msgid "&Helix"
-msgstr "&Helix"
+msgstr "&Hélice"
#: graphicswin.cpp:118
msgid "&Lathe"
-msgstr "&Lathe"
+msgstr "&Tour (révolution complète)"
#: graphicswin.cpp:119
msgid "Re&volve"
@@ -925,11 +940,11 @@ msgstr "Ré&volution"
#: graphicswin.cpp:121
msgid "Link / Assemble..."
-msgstr "Lié / Assembler..."
+msgstr "Lier / Assembler..."
#: graphicswin.cpp:122
msgid "Link Recent"
-msgstr "Lié Récent"
+msgstr "Lier Récent"
#: graphicswin.cpp:124
msgid "&Sketch"
@@ -973,7 +988,7 @@ msgstr "&Arc de Cercle"
#: graphicswin.cpp:136
msgid "&Bezier Cubic Spline"
-msgstr "Spline Cubique de &Beziers"
+msgstr "Spline Cubique de &Bézier"
#: graphicswin.cpp:138
msgid "&Text in TrueType Font"
@@ -985,11 +1000,11 @@ msgstr "&Image"
#: graphicswin.cpp:141
msgid "To&ggle Construction"
-msgstr "&Basculer en mode \"Construction\""
+msgstr "&Basculer en mode « Construction »"
#: graphicswin.cpp:142
msgid "Tangent &Arc at Point"
-msgstr "&Arc Tangent au Point"
+msgstr "Rendre l'&Arc Tangent au Point"
#: graphicswin.cpp:143
msgid "Split Curves at &Intersection"
@@ -997,7 +1012,7 @@ msgstr "Diviser les Courbes à l'&Intersection"
#: graphicswin.cpp:145
msgid "&Constrain"
-msgstr "&Constraintes"
+msgstr "&Contraintes"
#: graphicswin.cpp:146
msgid "&Distance / Diameter"
@@ -1037,15 +1052,15 @@ msgstr "&Sur Point / Courbe / Plan"
#: graphicswin.cpp:157
msgid "E&qual Length / Radius / Angle"
-msgstr "&Egale Longueur / Rayon / Angle"
+msgstr "É&galité Longueur / Rayon / Angle"
#: graphicswin.cpp:158
msgid "Length / Arc Ra&tio"
-msgstr ""
+msgstr "Ratio Longueur / Arc"
#: graphicswin.cpp:159
msgid "Length / Arc Diff&erence"
-msgstr ""
+msgstr "Différence Longueur / Arc"
#: graphicswin.cpp:160
msgid "At &Midpoint"
@@ -1081,15 +1096,15 @@ msgstr "&Analyse"
#: graphicswin.cpp:170
msgid "Measure &Volume"
-msgstr "Mesure &Volume"
+msgstr "Mesurer &Volume"
#: graphicswin.cpp:171
msgid "Measure A&rea"
-msgstr "Mesure &Aire"
+msgstr "Mesurer &Aire"
#: graphicswin.cpp:172
msgid "Measure &Perimeter"
-msgstr "Mesure &Périmètre"
+msgstr "Mesurer &Périmètre"
#: graphicswin.cpp:173
msgid "Show &Interfering Parts"
@@ -1113,11 +1128,11 @@ msgstr "&Tracer Point"
#: graphicswin.cpp:180
msgid "&Stop Tracing..."
-msgstr "&Arrêt Tracé..."
+msgstr "&Arrêter Tracé..."
#: graphicswin.cpp:181
msgid "Step &Dimension..."
-msgstr "Espacement &Dimension..."
+msgstr "&Dimension pas-à-pas..."
#: graphicswin.cpp:183
msgid "&Help"
@@ -1133,7 +1148,7 @@ msgstr "&Site web / Manuel"
#: graphicswin.cpp:186
msgid "&Go to GitHub commit"
-msgstr ""
+msgstr "Voir le commit sur &GitHub"
#: graphicswin.cpp:188
msgid "&About"
@@ -1146,11 +1161,11 @@ msgstr "(pas de fichier récent)"
#: graphicswin.cpp:370
#, c-format
msgid "File '%s' does not exist."
-msgstr ""
+msgstr "Le fichier « %s » n'existe pas."
#: graphicswin.cpp:737
msgid "No workplane is active, so the grid will not appear."
-msgstr "Pas de plan de travail actif, donc la grille ne va pas apparaître."
+msgstr "Pas de plan de travail actif, la grille ne va donc pas apparaître."
#: graphicswin.cpp:752
msgid ""
@@ -1192,16 +1207,16 @@ msgid ""
"to define the plane for the snap grid."
msgstr ""
"Aucun plan de travail n'est actif. Activez un plan de travail (avec Dessin -"
-"> Dans plan de travail) pour définir le plan pour la grille d'accrochage."
+"> Dans le plan de travail) pour définir le plan pour la grille d'accrochage."
#: graphicswin.cpp:1185
msgid ""
"Can't snap these items to grid; select points, text comments, or constraints "
"with a label. To snap a line, select its endpoints."
msgstr ""
-"Impossible d'accrocher ces éléments à la grille. Sélectionnez des points, "
-"des textes de commentaires ou des contraintes avec une étiquette. Pour "
-"accrocher une ligne, sélectionnez ses points d'extrémité."
+"Impossible d'accrocher ces éléments à la grille. Sélectionnez des points, des "
+"textes de commentaires ou des contraintes avec une étiquette. Pour accrocher une "
+"ligne, sélectionnez ses extrémités."
#: graphicswin.cpp:1270
msgid "No workplane selected. Activating default workplane for this group."
@@ -1217,7 +1232,7 @@ msgid ""
msgstr ""
"Aucun plan de travail n'est sélectionné et le groupe actif n'a pas de plan "
"de travail par défaut. Essayez de sélectionner un plan de travail ou "
-"d'activer un groupe de \"Dessin dans nouveau plan travail\"."
+"d'activer un groupe de « Dessin dans nouveau plan travail »."
#: graphicswin.cpp:1294
msgid ""
@@ -1229,9 +1244,7 @@ msgstr ""
#: graphicswin.cpp:1305
msgid "click point on arc (draws anti-clockwise)"
-msgstr ""
-"cliquez un point sur l'arc (dessine dans le sens inverse des aiguilles d'une "
-"montre)"
+msgstr "cliquez un point sur l'arc (dessine dans le sens anti-horaire)"
#: graphicswin.cpp:1306
msgid "click to place datum point"
@@ -1292,13 +1305,20 @@ msgid ""
" * a point and a normal (through the point, orthogonal to the normal)\n"
" * a workplane (copy of the workplane)\n"
msgstr ""
+"Mauvaise sélection pour un nouveau dessin. Ce groupe peut être créé avec :\n"
+"\n"
+" – Un point (passant par le point, orthogonal aux axes de coordonnées)\n"
+" – Un point et deux segments de lignes (passant par le point, parallèle aux "
+"deux lignes)\n"
+" – Un point et une normale (passant par le point, orthogonal à la normale)\n"
+" – Un plan de travail (copie du plan de travail)\n"
#: group.cpp:166
msgid ""
"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch "
"will be extruded normal to the workplane."
msgstr ""
-"Activez un plan de travail (Dessin -> Dans plan de travail) avant "
+"Activez un plan de travail (Dessin -> Dans le plan de travail) avant "
"l'extrusion. Le croquis sera extrudé normalement au plan de travail."
#: group.cpp:175
@@ -1308,7 +1328,7 @@ msgstr "extruder"
#: group.cpp:180
msgid "Lathe operation can only be applied to planar sketches."
-msgstr ""
+msgstr "L'opération tour ne peut être appliquée qu'à des dessins plans."
#: group.cpp:191
msgid ""
@@ -1319,20 +1339,20 @@ msgid ""
" * a line segment (revolved about line segment)\n"
msgstr ""
"Mauvaise sélection pour un nouveau groupe de révolution. Ce groupe peut être "
-"créé avec:\n"
+"créé avec :\n"
"\n"
-" * Un point et un segment de ligne ou normal (révolution autour d'un axe "
+" – Un point et un segment de ligne ou normal (révolution autour d'un axe "
"parallèle à la ligne / point normal, par le point)\n"
-" * Un segment de ligne (révolution sur le segment de ligne)\n"
+" – Un segment de ligne (révolution sur le segment de ligne)\n"
#: group.cpp:201
msgctxt "group-name"
msgid "lathe"
-msgstr "révolution"
+msgstr "tour"
#: group.cpp:206
msgid "Revolve operation can only be applied to planar sketches."
-msgstr ""
+msgstr "L'opération révolution ne peut être appliquée qu'à des dessins plans"
#: group.cpp:217
msgid ""
@@ -1342,15 +1362,21 @@ msgid ""
"to line / normal, through point)\n"
" * a line segment (revolved about line segment)\n"
msgstr ""
+"Mauvaise sélection pour un nouveau groupe de révolution. Ce groupe peut être créé "
+"avec :\n"
+"\n"
+" – Un point et un segment de ligne ou normal (révolution autour d'un axe "
+"parallèle à la ligne / point normal, par le point)\n"
+" – Un segment de ligne (révolution sur le segment de ligne)\n"
#: group.cpp:229
msgctxt "group-name"
msgid "revolve"
-msgstr ""
+msgstr "révolution"
#: group.cpp:234
msgid "Helix operation can only be applied to planar sketches."
-msgstr ""
+msgstr "L'opération hélice ne peut être appliquée qu'à des dessins plans"
#: group.cpp:245
msgid ""
@@ -1360,11 +1386,17 @@ msgid ""
"to line / normal, through point)\n"
" * a line segment (revolved about line segment)\n"
msgstr ""
+"Mauvaise sélection pour un nouveau groupe d'hélice. Ce groupe peut être créé "
+"avec :\n"
+"\n"
+" – Un point et un segment de ligne ou normal (révolution autour d'un axe "
+"parallèle à la ligne / point normal, par le point)\n"
+" – Un segment de ligne (révolution sur le segment de ligne)\n"
#: group.cpp:257
msgctxt "group-name"
msgid "helix"
-msgstr ""
+msgstr "hélice"
#: group.cpp:270
msgid ""
@@ -1376,11 +1408,11 @@ msgid ""
"point, and parallel to line / normal)\n"
msgstr ""
"Mauvaise sélection pour une nouvelle rotation. Ce groupe peut être créé "
-"avec:\n"
+"avec :\n"
"\n"
-" * Un point, lorsqu'il est verrouillé dans un plan de travail (rotation "
+" – Un point, lorsqu'il est verrouillé dans un plan de travail (rotation "
"dans le plan, autour de ce point)\n"
-" * Un point et une ligne ou une normale (tourner autour d'un axe par le "
+" – Un point et une ligne ou une normale (tourner autour d'un axe par le "
"point et parallèle à la ligne / normale)\n"
#: group.cpp:283
@@ -1399,23 +1431,23 @@ msgstr "(sans nom)"
#: groupmesh.cpp:707
msgid "not closed contour, or not all same style!"
-msgstr "contour non fermé ou tout n'est pas du même style!"
+msgstr "contour non fermé ou tout n'est pas du même style !"
#: groupmesh.cpp:720
msgid "points not all coplanar!"
-msgstr "les points ne sont pas tous coplanaires!"
+msgstr "les points ne sont pas tous coplanaires !"
#: groupmesh.cpp:722
msgid "contour is self-intersecting!"
-msgstr "le contour s'entrecroise!"
+msgstr "le contour s'entrecroise !"
#: groupmesh.cpp:724
msgid "zero-length edge!"
-msgstr "arête de longueur nulle!"
+msgstr "arête de longueur nulle !"
#: importmesh.cpp:136
msgid "Text-formated STL files are not currently supported"
-msgstr ""
+msgstr "Les fichiers STL textuels ne sont pas actuellement pas supportés"
#: modify.cpp:252
msgid "Must be sketching in workplane to create tangent arc."
@@ -1426,8 +1458,8 @@ msgid ""
"To create a tangent arc, select a point where two non-construction lines or "
"circles in this group and workplane join."
msgstr ""
-"Pour créer un arc tangent, sélectionnez un point où deux lignes (pas de "
-"construction) ou cercles de ce groupe et de ce plan se joignent."
+"Pour créer un arc tangent, sélectionnez un point où deux lignes ou cercles (pas de "
+"construction) de ce groupe et de ce plan se rejoignent."
#: modify.cpp:386
msgid ""
@@ -1440,7 +1472,7 @@ msgstr ""
#: modify.cpp:595
msgid "Couldn't split this entity; lines, circles, or cubics only."
msgstr ""
-"Impossible de diviser cette entité; Lignes, cercles ou cubiques uniquement."
+"Impossible de diviser cette entité ; Lignes, cercles ou cubiques uniquement."
#: modify.cpp:622
msgid "Must be sketching in workplane to split."
@@ -1451,10 +1483,12 @@ msgid ""
"Select two entities that intersect each other (e.g. two lines/circles/arcs "
"or a line/circle/arc and a point)."
msgstr ""
+"Sélectionnez deux entités qui s'intersectent (par exemple deux lignes/cercles/"
+"arcs, ou une ligne/cercle/arc et un point)."
#: modify.cpp:734
msgid "Can't split; no intersection found."
-msgstr "Impossible de diviser; pas d'intersection trouvée."
+msgstr "Impossible de diviser ; pas d'intersection trouvée."
#: mouse.cpp:557
msgid "Assign to Style"
@@ -1503,11 +1537,11 @@ msgstr "Ajouter un point à la Spline"
#: mouse.cpp:688
msgid "Cannot add spline point: maximum number of points reached."
msgstr ""
-"Impossible d'ajouter le point spline: nombre maximum de points atteints."
+"Impossible d'ajouter le point spline : nombre maximum de points atteints."
#: mouse.cpp:713
msgid "Toggle Construction"
-msgstr "Basculer en mode \"construction\"."
+msgstr "Basculer en mode « construction »."
#: mouse.cpp:729
msgid "Delete Point-Coincident Constraint"
@@ -1558,8 +1592,8 @@ msgid ""
"Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In "
"Workplane."
msgstr ""
-"Impossible de dessiner un rectangle en 3d; D'abord, activez un plan de "
-"travail avec \"Dessin -> Dans plan de travail\"."
+"Impossible de dessiner un rectangle en 3d ; D'abord, activez un plan de "
+"travail avec « Dessin -> Dans le plan de travail »."
#: mouse.cpp:1026
msgid "click to place other corner of rectangle"
@@ -1574,8 +1608,8 @@ msgid ""
"Can't draw arc in 3d; first, activate a workplane with Sketch -> In "
"Workplane."
msgstr ""
-"Ne peut pas dessiner l'arc en 3d; D'abord, activez un plan de travail avec "
-"\"Dessin -> Dans plan de travail\"."
+"Ne peut pas dessiner l'arc en 3d ; D'abord, activez un plan de travail avec "
+"« Dessin -> Dans le plan de travail »."
#: mouse.cpp:1071
msgid "click to place point"
@@ -1589,7 +1623,7 @@ msgstr "cliquez le prochain point cubique ou appuyez sur Esc"
msgid ""
"Sketching in a workplane already; sketch in 3d before creating new workplane."
msgstr ""
-"Vous dessinez déjà dans un plan de travail; Sélectionner \"Dessiner en 3d\" "
+"Vous dessinez déjà dans un plan de travail ; Sélectionner « Dessiner en 3d » "
"avant de créer un nouveau plan de travail."
#: mouse.cpp:1108
@@ -1597,115 +1631,115 @@ msgid ""
"Can't draw text in 3d; first, activate a workplane with Sketch -> In "
"Workplane."
msgstr ""
-"Impossible de dessiner du texte en 3d; D'abord, activer un plan de travail "
-"avec \"Dessin -> Dans plan de travail\"."
+"Impossible de dessiner du texte en 3d ; D'abord, activer un plan de travail "
+"avec « Dessin -> Dans le plan de travail »."
#: mouse.cpp:1125
msgid "click to place bottom right of text"
-msgstr ""
+msgstr "cliquer pour placer le coin inférieur droit du texte"
#: mouse.cpp:1131
msgid ""
"Can't draw image in 3d; first, activate a workplane with Sketch -> In "
"Workplane."
msgstr ""
-"Impossible de dessiner l'image en 3d; D'abord, activez un plan de travail "
-"avec \"Dessin -> Dans plan de travail\"."
+"Impossible de dessiner l'image en 3d ; D'abord, activez un plan de travail "
+"avec « Dessin -> Dans le plan de travail »."
#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:553
msgctxt "file-type"
msgid "SolveSpace models"
-msgstr ""
+msgstr "modèles Solvespace"
#: platform/gui.cpp:89
msgctxt "file-type"
msgid "ALL"
-msgstr ""
+msgstr "TOUT"
#: platform/gui.cpp:91
msgctxt "file-type"
msgid "IDF circuit board"
-msgstr ""
+msgstr "circuit imprimé IDF"
#: platform/gui.cpp:92
msgctxt "file-type"
msgid "STL triangle mesh"
-msgstr ""
+msgstr "maillage de triangles STL"
#: platform/gui.cpp:96
msgctxt "file-type"
msgid "PNG image"
-msgstr ""
+msgstr "image PNG"
#: platform/gui.cpp:100
msgctxt "file-type"
msgid "STL mesh"
-msgstr ""
+msgstr "maillage STL"
#: platform/gui.cpp:101
msgctxt "file-type"
msgid "Wavefront OBJ mesh"
-msgstr ""
+msgstr "Mesh Wavefront OBJ"
#: platform/gui.cpp:102
msgctxt "file-type"
msgid "Three.js-compatible mesh, with viewer"
-msgstr ""
+msgstr "Mesh compatible avec Three.js, avec visionneuse"
#: platform/gui.cpp:103
msgctxt "file-type"
msgid "Three.js-compatible mesh, mesh only"
-msgstr ""
+msgstr "Mesh compatible avec Three.js, mesh seulement"
#: platform/gui.cpp:104
msgctxt "file-type"
msgid "VRML text file"
-msgstr ""
+msgstr "Fichier texte VRML"
#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122
msgctxt "file-type"
msgid "STEP file"
-msgstr ""
+msgstr "fichier STEP"
#: platform/gui.cpp:112
msgctxt "file-type"
msgid "PDF file"
-msgstr ""
+msgstr "fichier PDF"
#: platform/gui.cpp:113
msgctxt "file-type"
msgid "Encapsulated PostScript"
-msgstr ""
+msgstr "PostScript encapsulé"
#: platform/gui.cpp:114
msgctxt "file-type"
msgid "Scalable Vector Graphics"
-msgstr ""
+msgstr "image vectorielle SVG"
#: platform/gui.cpp:116 platform/gui.cpp:123
msgctxt "file-type"
msgid "DXF file (AutoCAD 2007)"
-msgstr ""
+msgstr "fichier DXF (AutoCAD 2007)"
#: platform/gui.cpp:117
msgctxt "file-type"
msgid "HPGL file"
-msgstr ""
+msgstr "fichier HPGL"
#: platform/gui.cpp:118
msgctxt "file-type"
msgid "G Code"
-msgstr ""
+msgstr "G Code"
#: platform/gui.cpp:127
msgctxt "file-type"
msgid "AutoCAD DXF and DWG files"
-msgstr ""
+msgstr "fichiers AutoCAD DXF et DWG"
#: platform/gui.cpp:131
msgctxt "file-type"
msgid "Comma-separated values"
-msgstr ""
+msgstr "CSV (Comma-separated values)"
#: platform/guigtk.cpp:1382 platform/guimac.mm:1509 platform/guiwin.cpp:1641
msgid "untitled"
@@ -1736,7 +1770,7 @@ msgstr "_Sauver"
#: platform/guigtk.cpp:1399 platform/guigtk.cpp:1432
msgctxt "button"
msgid "_Open"
-msgstr ""
+msgstr "_Ouvrir"
#: solvespace.cpp:171
msgctxt "title"
@@ -1746,22 +1780,22 @@ msgstr "Sauvegarde automatique existante"
#: solvespace.cpp:172
msgctxt "dialog"
msgid "An autosave file is available for this sketch."
-msgstr ""
+msgstr "Un fichier de sauvegarder automatique est disponible pour ce dessin"
#: solvespace.cpp:173
msgctxt "dialog"
msgid "Do you want to load the autosave file instead?"
-msgstr ""
+msgstr "Voulez-vous charger la sauvegarde automatique à la place ?"
#: solvespace.cpp:174
msgctxt "button"
msgid "&Load autosave"
-msgstr ""
+msgstr "&Charger la sauvegarde automatique"
#: solvespace.cpp:176
msgctxt "button"
msgid "Do&n't Load"
-msgstr ""
+msgstr "Ne &pas la charger"
#: solvespace.cpp:599
msgctxt "title"
@@ -1772,27 +1806,27 @@ msgstr "Fichier modifié"
#, c-format
msgctxt "dialog"
msgid "Do you want to save the changes you made to the sketch “%s”?"
-msgstr ""
+msgstr "Voulez-vous sauver les changements que vous avez fait au dessin « %s » ?"
#: solvespace.cpp:604
msgctxt "dialog"
msgid "Do you want to save the changes you made to the new sketch?"
-msgstr ""
+msgstr "Voulez-vous sauver les changements que vous avez fait au nouveau dessin ?"
#: solvespace.cpp:607
msgctxt "dialog"
msgid "Your changes will be lost if you don't save them."
-msgstr ""
+msgstr "Vos changements seront perdus si vous ne les sauvez pas."
#: solvespace.cpp:608
msgctxt "button"
msgid "&Save"
-msgstr ""
+msgstr "&Sauver"
#: solvespace.cpp:610
msgctxt "button"
msgid "Do&n't Save"
-msgstr ""
+msgstr "&Ne pas sauver"
#: solvespace.cpp:631
msgctxt "title"
@@ -1810,6 +1844,9 @@ msgid ""
"is probably not what you want; hide them by clicking the link at the top of "
"the text window."
msgstr ""
+"Les contraintes sont actuellement affichées, et seront exportées dans le toolpath. "
+"Ce n'est probablement pas ce que vous voulez, cachez-le en cliquer le lien en haut "
+"de la fenêtre de texte."
#: solvespace.cpp:772
#, c-format
@@ -1817,18 +1854,21 @@ msgid ""
"Can't identify file type from file extension of filename '%s'; try .dxf or ."
"dwg."
msgstr ""
+"Impossible d'identifier le type de fichier à partir de l'extension '%s' ; essayez ."
+"dxf ou .dwg."
#: solvespace.cpp:824
msgid "Constraint must have a label, and must not be a reference dimension."
-msgstr ""
+msgstr "La contrainte doit avoir un nom, et ne doit pas référencer une dimension."
#: solvespace.cpp:828
msgid "Bad selection for step dimension; select a constraint."
msgstr ""
+"Mauvaise sélection pour le pas-à-pas dimension ; sélectionnea une contrainte."
#: solvespace.cpp:852
msgid "The assembly does not interfere, good."
-msgstr ""
+msgstr "Cet assemblage n'interfère pas, bien."
#: solvespace.cpp:868
#, c-format
@@ -1837,6 +1877,9 @@ msgid ""
"\n"
" %s"
msgstr ""
+"Le volume du modèle de solide est :\n"
+"\n"
+" %s"
#: solvespace.cpp:877
#, c-format
@@ -1846,6 +1889,10 @@ msgid ""
"\n"
" %s"
msgstr ""
+"\n"
+"Le volume de ce groupe mesh est :\n"
+"\n"
+" %s"
#: solvespace.cpp:882
msgid ""
@@ -1854,6 +1901,10 @@ msgid ""
"Curved surfaces have been approximated as triangles.\n"
"This introduces error, typically of around 1%."
msgstr ""
+"\n"
+"\n"
+"Les surfaces courbes ont été approximée par des triangles.\n"
+"Cela introduit une erreur, d'environ 1% en général."
#: solvespace.cpp:897
#, c-format
@@ -1865,12 +1916,20 @@ msgid ""
"Curves have been approximated as piecewise linear.\n"
"This introduces error, typically of around 1%%."
msgstr ""
+"La surface des faces sélectionnées est :\n"
+"\n"
+" %s\n"
+"\n"
+"Les courbes ont été approximée comme étant linéaires par morceau.\n"
+"Cela introduit une erreur, d'environ 1%% en général."
#: solvespace.cpp:906
msgid ""
"This group does not contain a correctly-formed 2d closed area. It is open, "
"not coplanar, or self-intersecting."
msgstr ""
+"Ce groupe ne contient pas de surface 3d fermée correcte. Elle est ouverte, non "
+"coplanaire, ou s'intersecte."
#: solvespace.cpp:918
#, c-format
@@ -1882,6 +1941,12 @@ msgid ""
"Curves have been approximated as piecewise linear.\n"
"This introduces error, typically of around 1%%."
msgstr ""
+"La surface de la région dessinée dans ce groupe est :\n"
+"\n"
+" %s\n"
+"\n"
+"Les courbes ont été approximée comme linéaires par morceau.\n"
+"Cela introduit une erreur, d'environ 1%% en général."
#: solvespace.cpp:938
#, c-format
@@ -1893,35 +1958,44 @@ msgid ""
"Curves have been approximated as piecewise linear.\n"
"This introduces error, typically of around 1%%."
msgstr ""
+"La longueur totale des entités sélectionnées est :\n"
+"\n"
+" %s\n"
+"\n"
+"\n"
+"Les courbes ont été approximée comme linéaires par morceau.\n"
+"Cela introduit une erreur, d'environ 1%% en général."
#: solvespace.cpp:944
msgid "Bad selection for perimeter; select line segments, arcs, and curves."
msgstr ""
+"Mauvaise sélection pour un périmètre ; sélectionnez des segments de ligne, des "
+"arcs, et des coubres."
#: solvespace.cpp:960
msgid "Bad selection for trace; select a single point."
-msgstr ""
+msgstr "Mauvaise sélection pour le tracé ; sélectionnez un unique point."
#: solvespace.cpp:987
#, c-format
msgid "Couldn't write to '%s'"
-msgstr ""
+msgstr "Impossible d'écrire « %s »"
#: solvespace.cpp:1017
msgid "The mesh is self-intersecting (NOT okay, invalid)."
-msgstr ""
+msgstr "Le mesh s'intersecte (NON valide)."
#: solvespace.cpp:1018
msgid "The mesh is not self-intersecting (okay, valid)."
-msgstr ""
+msgstr "Le mesh ne s'intersecte pas (valide)."
#: solvespace.cpp:1020
msgid "The mesh has naked edges (NOT okay, invalid)."
-msgstr ""
+msgstr "Le mesh a des arêtes nues (NON valide)."
#: solvespace.cpp:1021
msgid "The mesh is watertight (okay, valid)."
-msgstr ""
+msgstr "Le mesh est étanche (valide)."
#: solvespace.cpp:1024
#, c-format
@@ -1930,6 +2004,9 @@ msgid ""
"\n"
"The model contains %d triangles, from %d surfaces."
msgstr ""
+"\n"
+"\n"
+"Le modèle contient %d triangles, répartis sur %d surfaces."
#: solvespace.cpp:1028
#, c-format
@@ -1940,6 +2017,11 @@ msgid ""
"\n"
"Zero problematic edges, good.%s"
msgstr ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"Pas d'arête problématique, bien.%s"
#: solvespace.cpp:1031
#, c-format
@@ -1950,6 +2032,11 @@ msgid ""
"\n"
"%d problematic edges, bad.%s"
msgstr ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"%d arêtes problématiques, pas bien.%s"
#: solvespace.cpp:1044
#, c-format
@@ -1967,13 +2054,25 @@ msgid ""
"\n"
"© 2008-%d Jonathan Westhues and other authors.\n"
msgstr ""
+"Ceci est SolveSpace version %s.\n"
+"\n"
+"Pour plus d'information, visitez http://solvespace.com/\n"
+"\n"
+"SolveSpace est un logiciel libre : vous êtes libre de le\n"
+"modifier et/ou le redistribuer selon les termes de la GNU\n"
+"General Public License (GPL) version 3 ou supérieure.\n"
+"\n"
+"Il n'y a AUCUNE GARANTIE, dans les limites légales.\n"
+"Pour plus de détails, visitez http://gnu.org/licenses/\n"
+"\n"
+"© 2008-%d Jonathan Westhues et autres auteurs.\n"
#: style.cpp:185
msgid ""
"Can't assign style to an entity that's derived from another entity; try "
"assigning a style to this entity's parent."
msgstr ""
-"Impossible d'attribuer le style à une entité dérivée d'une autre entité; "
+"Impossible d'attribuer le style à une entité dérivée d'une autre entité ; "
"Essayez d'attribuer un style au parent de cette entité."
#: style.cpp:735
@@ -2030,7 +2129,7 @@ msgstr "Créer un arc tangent au point sélectionné"
#: toolbar.cpp:32
msgid "Sketch cubic Bezier spline"
-msgstr "Dessin d'une spline cubique de Bezier"
+msgstr "Dessin d'une spline cubique de Bézier"
#: toolbar.cpp:34
msgid "Sketch datum point"
@@ -2038,7 +2137,7 @@ msgstr "Dessin d'un point"
#: toolbar.cpp:36
msgid "Toggle construction"
-msgstr "Basculer en mode \"construction\""
+msgstr "Basculer en mode « construction »"
#: toolbar.cpp:38
msgid "Split lines / curves where they intersect"
@@ -2098,15 +2197,15 @@ msgstr "Nouveau groupe d'extrusion du dessin actif"
#: toolbar.cpp:70
msgid "New group rotating active sketch"
-msgstr "Nouveau groupe de révolution du dessin actif"
+msgstr "Nouveau groupe de révolution du dessin actif (tour complet)"
#: toolbar.cpp:72
msgid "New group helix from active sketch"
-msgstr ""
+msgstr "Nouveau groupe hélice du dessin actif"
#: toolbar.cpp:74
msgid "New group revolve active sketch"
-msgstr ""
+msgstr "Nouveau groupe de révolution partielle du dessin actif"
#: toolbar.cpp:76
msgid "New group step and repeat rotating"
@@ -2150,7 +2249,7 @@ msgstr "Message"
#: util.cpp:170
msgctxt "button"
msgid "&OK"
-msgstr ""
+msgstr "&Ok"
#: view.cpp:127
msgid "Scale cannot be zero or negative."
@@ -2158,7 +2257,7 @@ msgstr "L'échelle ne peut pas être zéro ou négative."
#: view.cpp:139 view.cpp:148
msgid "Bad format: specify x, y, z"
-msgstr "Mauvais format: Spécifiez x, y, z"
+msgstr "Mauvais format : Spécifiez x, y, z"
#~ msgid ""
#~ "Bad selection for length ratio constraint. This constraint can apply to:\n"
@@ -2166,7 +2265,7 @@ msgstr "Mauvais format: Spécifiez x, y, z"
#~ " * two line segments\n"
#~ msgstr ""
#~ "Mauvaise sélection pour la contrainte du rapport de longueur. Cette "
-#~ "contrainte peut s'appliquer à:\n"
+#~ "contrainte peut s'appliquer à :\n"
#~ "\n"
#~ " * Deux segments de ligne\n"
@@ -2177,7 +2276,7 @@ msgstr "Mauvais format: Spécifiez x, y, z"
#~ " * two line segments\n"
#~ msgstr ""
#~ "Mauvaise sélection pour la contrainte de différence de longueur. Cette "
-#~ "contrainte peut s'appliquer à:\n"
+#~ "contrainte peut s'appliquer à :\n"
#~ "\n"
#~ " * Deux segments de ligne\n"
@@ -2197,7 +2296,7 @@ msgstr "Mauvais format: Spécifiez x, y, z"
#~ " * a workplane (copy of the workplane)\n"
#~ msgstr ""
#~ "Mauvaise sélection pour un nouveau dessin dans le plan de travail. Ce "
-#~ "groupe peut être créé avec:\n"
+#~ "groupe peut être créé avec :\n"
#~ "\n"
#~ " * Un point (par le point, orthogonal aux axes de coordonnées)\n"
#~ " * Un point et deux segments de ligne (par le point, parallèle aux "
@@ -2216,7 +2315,7 @@ msgstr "Mauvais format: Spécifiez x, y, z"
#~ msgid "Do you want to save the changes you made to the new sketch?"
#~ msgstr ""
#~ "Voulez-vous enregistrer les modifications que vous avez apportées au "
-#~ "nouveau dessin?"
+#~ "nouveau dessin ?"
#~ msgid "Your changes will be lost if you don't save them."
#~ msgstr "Vos modifications seront perdues si vous ne les enregistrez pas."
@@ -2234,7 +2333,7 @@ msgstr "Mauvais format: Spécifiez x, y, z"
#~ msgstr "Ne pas sauver"
#~ msgid "Do you want to load the autosave file instead?"
-#~ msgstr "Voulez-vous charger le fichier de sauvegarde à la place?"
+#~ msgstr "Voulez-vous charger le fichier de sauvegarde à la place ?"
#~ msgctxt "button"
#~ msgid "Load"
@@ -2249,8 +2348,8 @@ msgstr "Mauvais format: Spécifiez x, y, z"
#~ "If you select “No”, any geometry that depends on the missing file will be "
#~ "removed."
#~ msgstr ""
-#~ "Voulez-vous le localiser manuellement?\n"
-#~ "Si vous sélectionnez \"Non\", toute géométrie qui dépend du fichier "
+#~ "Voulez-vous le localiser manuellement ?\n"
+#~ "Si vous sélectionnez « Non », toute géométrie qui dépend du fichier "
#~ "manquant sera supprimée."
#~ msgctxt "button"
@@ -2278,7 +2377,7 @@ msgstr "Mauvais format: Spécifiez x, y, z"
#~ msgstr ""
#~ "Le fichier a changé depuis sa dernière sauvegarde.\n"
#~ "\n"
-#~ "Voulez-vous enregistrer les modifications?"
+#~ "Voulez-vous enregistrer les modifications ?"
#~ msgctxt "button"
#~ msgid "Do_n't Save"
diff --git a/res/locales/ja_JP.po b/res/locales/ja_JP.po
new file mode 100644
index 00000000..0e4559eb
--- /dev/null
+++ b/res/locales/ja_JP.po
@@ -0,0 +1,2138 @@
+# Japanese translations for SolveSpace package.
+# Copyright (C) 2022 the Solvespace authors
+# This file is distributed under the same license as the SolveSpace package.
+# verylowfreq, 2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: SolveSpace 3.0\n"
+"Report-Msgid-Bugs-To: whitequark@whitequark.org\n"
+"POT-Creation-Date: 2022-02-01 16:24+0200\n"
+"PO-Revision-Date: 2022-06-13 20:47+0900\n"
+"Last-Translator: https://github.com/verylowfreq\n"
+"Language-Team: \n"
+"Language: ja_JP\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 3.1\n"
+
+#: clipboard.cpp:309
+msgid ""
+"Cut, paste, and copy work only in a workplane.\n"
+"\n"
+"Activate one with Sketch -> In Workplane."
+msgstr ""
+"切り取り・貼り付け・コピーは作業平面でのみ利用できます。\n"
+"\n"
+"作業平面は「スケッチ -> 作業平面上でスケッチする」から指定してください。"
+
+#: clipboard.cpp:326
+msgid "Clipboard is empty; nothing to paste."
+msgstr "クリップボードは空です。貼り付けるものはありません。"
+
+#: clipboard.cpp:373
+msgid "Number of copies to paste must be at least one."
+msgstr "貼り付ける個数は1以上でなければなりません。"
+
+#: clipboard.cpp:389 textscreens.cpp:833
+msgid "Scale cannot be zero."
+msgstr "縮尺は0にできません。"
+
+#: clipboard.cpp:431
+msgid "Select one point to define origin of rotation."
+msgstr "回転の原点を決めるために、点を1つ選択してください。"
+
+#: clipboard.cpp:443
+msgid "Select two points to define translation vector."
+msgstr "移動方向を決めるために、点を2つ選択してください。"
+
+#: clipboard.cpp:453
+msgid "Transformation is identity. So all copies will be exactly on top of each other."
+msgstr "移動量が 0 です。すべての複製は同一位置にあります。"
+
+#: clipboard.cpp:457
+msgid "Too many items to paste; split this into smaller pastes."
+msgstr "複製の数が多すぎます。分割してください。"
+
+#: clipboard.cpp:462
+msgid "No workplane active."
+msgstr "作業平面がアクティブでありません。"
+
+#: confscreen.cpp:381
+msgid "Bad format: specify coordinates as x, y, z"
+msgstr "書式が誤っています。座標を \"x, y, z\" で指定してください。"
+
+#: confscreen.cpp:391 style.cpp:729 textscreens.cpp:864
+msgid "Bad format: specify color as r, g, b"
+msgstr "書式が誤っています。色を \"r, g, b\" で指定してください。"
+
+#: confscreen.cpp:417
+msgid ""
+"The perspective factor will have no effect until you enable View -> Use Perspective Projection."
+msgstr "透視投影の拡大率は、「表示 -> 透視投影で表示」を有効にするまで反映されません。"
+
+#: confscreen.cpp:435 confscreen.cpp:445
+#, c-format
+msgid "Specify between 0 and %d digits after the decimal."
+msgstr "小数点以下の桁数は、0 から %d の範囲で指定してください。"
+
+#: confscreen.cpp:457
+msgid "Export scale must not be zero!"
+msgstr "エクスポートの比率には0を指定できません。"
+
+#: confscreen.cpp:469
+msgid "Cutter radius offset must not be negative!"
+msgstr "ドリル径は負の値にできません。"
+
+#: confscreen.cpp:528
+msgid "Bad value: autosave interval should be positive"
+msgstr "値が誤っています。オートセーブの間隔は正の値を指定してください。"
+
+#: confscreen.cpp:531
+msgid "Bad format: specify interval in integral minutes"
+msgstr "書式が誤っています。間隔は分単位で整数を指定してください。"
+
+#: constraint.cpp:12
+msgctxt "constr-name"
+msgid "pts-coincident"
+msgstr ""
+
+#: constraint.cpp:13
+msgctxt "constr-name"
+msgid "pt-pt-distance"
+msgstr ""
+
+#: constraint.cpp:14
+msgctxt "constr-name"
+msgid "pt-line-distance"
+msgstr ""
+
+#: constraint.cpp:15
+msgctxt "constr-name"
+msgid "pt-plane-distance"
+msgstr ""
+
+#: constraint.cpp:16
+msgctxt "constr-name"
+msgid "pt-face-distance"
+msgstr ""
+
+#: constraint.cpp:17
+msgctxt "constr-name"
+msgid "proj-pt-pt-distance"
+msgstr ""
+
+#: constraint.cpp:18
+msgctxt "constr-name"
+msgid "pt-in-plane"
+msgstr ""
+
+#: constraint.cpp:19
+msgctxt "constr-name"
+msgid "pt-on-line"
+msgstr ""
+
+#: constraint.cpp:20
+msgctxt "constr-name"
+msgid "pt-on-face"
+msgstr ""
+
+#: constraint.cpp:21
+msgctxt "constr-name"
+msgid "eq-length"
+msgstr ""
+
+#: constraint.cpp:22
+msgctxt "constr-name"
+msgid "eq-length-and-pt-ln-dist"
+msgstr ""
+
+#: constraint.cpp:23
+msgctxt "constr-name"
+msgid "eq-pt-line-distances"
+msgstr ""
+
+#: constraint.cpp:24
+msgctxt "constr-name"
+msgid "length-ratio"
+msgstr ""
+
+#: constraint.cpp:25
+msgctxt "constr-name"
+msgid "arc-arc-length-ratio"
+msgstr ""
+
+#: constraint.cpp:26
+msgctxt "constr-name"
+msgid "arc-line-length-ratio"
+msgstr ""
+
+#: constraint.cpp:27
+msgctxt "constr-name"
+msgid "length-difference"
+msgstr ""
+
+#: constraint.cpp:28
+msgctxt "constr-name"
+msgid "arc-arc-len-difference"
+msgstr ""
+
+#: constraint.cpp:29
+msgctxt "constr-name"
+msgid "arc-line-len-difference"
+msgstr ""
+
+#: constraint.cpp:30
+msgctxt "constr-name"
+msgid "symmetric"
+msgstr ""
+
+#: constraint.cpp:31
+msgctxt "constr-name"
+msgid "symmetric-h"
+msgstr ""
+
+#: constraint.cpp:32
+msgctxt "constr-name"
+msgid "symmetric-v"
+msgstr ""
+
+#: constraint.cpp:33
+msgctxt "constr-name"
+msgid "symmetric-line"
+msgstr ""
+
+#: constraint.cpp:34
+msgctxt "constr-name"
+msgid "at-midpoint"
+msgstr ""
+
+#: constraint.cpp:35
+msgctxt "constr-name"
+msgid "horizontal"
+msgstr ""
+
+#: constraint.cpp:36
+msgctxt "constr-name"
+msgid "vertical"
+msgstr ""
+
+#: constraint.cpp:37
+msgctxt "constr-name"
+msgid "diameter"
+msgstr ""
+
+#: constraint.cpp:38
+msgctxt "constr-name"
+msgid "pt-on-circle"
+msgstr ""
+
+#: constraint.cpp:39
+msgctxt "constr-name"
+msgid "same-orientation"
+msgstr ""
+
+#: constraint.cpp:40
+msgctxt "constr-name"
+msgid "angle"
+msgstr ""
+
+#: constraint.cpp:41
+msgctxt "constr-name"
+msgid "parallel"
+msgstr ""
+
+#: constraint.cpp:42
+msgctxt "constr-name"
+msgid "arc-line-tangent"
+msgstr ""
+
+#: constraint.cpp:43
+msgctxt "constr-name"
+msgid "cubic-line-tangent"
+msgstr ""
+
+#: constraint.cpp:44
+msgctxt "constr-name"
+msgid "curve-curve-tangent"
+msgstr ""
+
+#: constraint.cpp:45
+msgctxt "constr-name"
+msgid "perpendicular"
+msgstr ""
+
+#: constraint.cpp:46
+msgctxt "constr-name"
+msgid "eq-radius"
+msgstr ""
+
+#: constraint.cpp:47
+msgctxt "constr-name"
+msgid "eq-angle"
+msgstr ""
+
+#: constraint.cpp:48
+msgctxt "constr-name"
+msgid "eq-line-len-arc-len"
+msgstr ""
+
+#: constraint.cpp:49
+msgctxt "constr-name"
+msgid "lock-where-dragged"
+msgstr ""
+
+#: constraint.cpp:50
+msgctxt "constr-name"
+msgid "comment"
+msgstr ""
+
+#: constraint.cpp:144
+msgid ""
+"The tangent arc and line segment must share an endpoint. Constrain them with Constrain -> On "
+"Point before constraining tangent."
+msgstr ""
+"正接する円弧と線分は端点を共有している必要があります。正接拘束をする前に「拘束 -> 一致」で拘束して"
+"ください。"
+
+#: constraint.cpp:163
+msgid ""
+"The tangent cubic and line segment must share an endpoint. Constrain them with Constrain -> On "
+"Point before constraining tangent."
+msgstr ""
+"ベジェ曲線と線分は端点を共有している必要があります。正接拘束をする前に「拘束 -> 一致」で拘束してく"
+"ださい。"
+
+#: constraint.cpp:189
+msgid ""
+"The curves must share an endpoint. Constrain them with Constrain -> On Point before constraining "
+"tangent."
+msgstr ""
+"曲線同士は端点を共有している必要があります。正接拘束をする前に「拘束 -> 一致」で拘束してください。"
+
+#: constraint.cpp:238
+msgid ""
+"Bad selection for distance / diameter constraint. This constraint can apply to:\n"
+"\n"
+" * two points (distance between points)\n"
+" * a line segment (length)\n"
+" * two points and a line segment or normal (projected distance)\n"
+" * a workplane and a point (minimum distance)\n"
+" * a line segment and a point (minimum distance)\n"
+" * a plane face and a point (minimum distance)\n"
+" * a circle or an arc (diameter)\n"
+msgstr ""
+"距離・直径拘束ができませんでした。次の要素に適用できます:\n"
+"\n"
+" * 2 つの点 (点同士の距離)\n"
+" * 1 つの線分 (長さ)\n"
+" * 2 つの点と、1 つの線分もしくは法線 (投影された距離)\n"
+" * 1 つの作業平面と 1 つの点 (最小の距離)\n"
+" * 1 つの線分と 1 つの点 (最小の距離)\n"
+" * 1 つの平面フェイスと1つの点 (最小の距離)\n"
+" * 1 つの円もしくは円弧 (直径)\n"
+"\n"
+
+#: constraint.cpp:291
+msgid ""
+"Bad selection for on point / curve / plane constraint. This constraint can apply to:\n"
+"\n"
+" * two points (points coincident)\n"
+" * a point and a workplane (point in plane)\n"
+" * a point and a line segment (point on line)\n"
+" * a point and a circle or arc (point on curve)\n"
+" * a point and a plane face (point on face)\n"
+msgstr ""
+"一致拘束ができませんでした。次の要素に適用できます:\n"
+"\n"
+" * 2 つの点 (点を同一位置に)\n"
+" * 1 つの点と 1 つの作業平面 (点を平面上に)\n"
+" * 1 つの点と 1 つの線分 (点を直線状に)\n"
+" * 1 つの点と、1つの円もしくは円弧 (点を曲線上に)\n"
+" * 1 つの点と 1 つの平面フェイス (点をフェイス上に)\n"
+
+#: constraint.cpp:353
+msgid ""
+"Bad selection for equal length / radius constraint. This constraint can apply to:\n"
+"\n"
+" * two line segments (equal length)\n"
+" * two line segments and two points (equal point-line distances)\n"
+" * a line segment and two points (equal point-line distances)\n"
+" * a line segment, and a point and line segment (point-line distance equals length)\n"
+" * four line segments or normals (equal angle between A,B and C,D)\n"
+" * three line segments or normals (equal angle between A,B and B,C)\n"
+" * two circles or arcs (equal radius)\n"
+" * a line segment and an arc (line segment length equals arc length)\n"
+msgstr ""
+"長さ・半径・角度の同値拘束ができませんでした。次の要素に適用できます:\n"
+"\n"
+" * 2 つの線分 (長さを同じに)\n"
+" * 2 つの線分と 2 つの点 (それぞれ直線と点の距離を同じに)\n"
+" * 1 つの線分と 2 つの点 (直線とそれぞれの点の距離を同じに)\n"
+" * 1 つの線分と、1 つの点と 1 つの線分 (線分の長さと、直線と点の距離を同じに)\n"
+" * 4つの線分もしくは法線 (直線A,B、直線C,Dそれぞれの交差する角度を同じに)\n"
+" * 3 つの線分もしくは法線 (直線A,B、直線B,Cそれぞれの交差する角度を同じに)\n"
+" * 2 つの円もしくは円弧 (半径を同じに)\n"
+" * 1 つの線分と 1 つの円弧 (線分の長さと円弧の長さを同じに)\n"
+
+#: constraint.cpp:407
+msgid ""
+"Bad selection for length ratio constraint. This constraint can apply to:\n"
+"\n"
+" * two line segments\n"
+" * two arcs\n"
+" * one arc and one line segment\n"
+msgstr ""
+"長さの比率の拘束ができませんでした。次の要素に適用できます:\n"
+"\n"
+" * 2 つの線分\n"
+" * 2 つの円弧\n"
+" * 円弧と線分\n"
+
+#: constraint.cpp:441
+msgid ""
+"Bad selection for length difference constraint. This constraint can apply to:\n"
+"\n"
+" * two line segments\n"
+" * two arcs\n"
+" * one arc and one line segment\n"
+msgstr ""
+"長さの差の拘束ができませんでした。次の要素に適用できます:\n"
+"\n"
+" * 2 つの線分\n"
+" * 2 つの円弧\n"
+" * 円弧と線分\n"
+
+#: constraint.cpp:472
+msgid ""
+"Bad selection for at midpoint constraint. This constraint can apply to:\n"
+"\n"
+" * a line segment and a point (point at midpoint)\n"
+" * a line segment and a workplane (line's midpoint on plane)\n"
+msgstr ""
+"中点拘束できませんでした。次の要素に適用できます:\n"
+"\n"
+" * 線分と点 (点を直線の中点に置きます)\n"
+" * 線分と作業平面 (直線の中点を作業平面上に置きます)\n"
+
+#: constraint.cpp:530
+msgid ""
+"Bad selection for symmetric constraint. This constraint can apply to:\n"
+"\n"
+" * two points or a line segment (symmetric about workplane's coordinate axis)\n"
+" * line segment, and two points or a line segment (symmetric about line segment)\n"
+" * workplane, and two points or a line segment (symmetric about workplane)\n"
+msgstr ""
+"対象拘束ができませんでした。次の要素の適用できます:\n"
+"\n"
+" * 2 つの点、もしくは 1 つの線分 (作業平面の座標軸について対称をとります)\n"
+" * 線分と、2 つの点もしくは 1 つの線分 (線分について対称をとります)\n"
+" * 作業平面と、2 つの点もしくは 1 つの線分 (作業平面について対称をとります)\n"
+
+#: constraint.cpp:545
+msgid "A workplane must be active when constraining symmetric without an explicit symmetry plane."
+msgstr "明示的に対称平面を指定しない場合は、対象拘束をするには作業平面をアクティブにしてください。"
+
+#: constraint.cpp:579
+msgid ""
+"Activate a workplane (with Sketch -> In Workplane) before applying a horizontal or vertical "
+"constraint."
+msgstr ""
+"垂直拘束や水平拘束をする前に、「スケッチ -> 作業平面上でスケッチする」で作業平面をアクティブにして"
+"ください。"
+
+#: constraint.cpp:592
+msgid ""
+"Bad selection for horizontal / vertical constraint. This constraint can apply to:\n"
+"\n"
+" * two points\n"
+" * a line segment\n"
+msgstr ""
+"垂直・水平拘束ができませんでした。次の要素に適用できます:\n"
+"\n"
+" * 2 つの点\n"
+" * 1 つの線分\n"
+
+#: constraint.cpp:613
+msgid ""
+"Bad selection for same orientation constraint. This constraint can apply to:\n"
+"\n"
+" * two normals\n"
+msgstr ""
+"同じ方向の拘束ができませんでした。次の要素に適用できます。\n"
+"\n"
+" * 2 つの法線\n"
+
+#: constraint.cpp:663
+msgid "Must select an angle constraint."
+msgstr "角度拘束を選択してください。"
+
+#: constraint.cpp:676
+msgid "Must select a constraint with associated label."
+msgstr "値を持つ拘束を選択してください。"
+
+#: constraint.cpp:687
+msgid ""
+"Bad selection for angle constraint. This constraint can apply to:\n"
+"\n"
+" * two line segments\n"
+" * a line segment and a normal\n"
+" * two normals\n"
+msgstr ""
+"角度拘束ができませんでした。次の要素に適用できます:\n"
+"\n"
+" * 2 つの線分\n"
+" * 線分と法線\n"
+" * 2 つの法線\n"
+
+#: constraint.cpp:754
+msgid "Curve-curve tangency must apply in workplane."
+msgstr "曲線同士の正接は作業平面上で適用できます。"
+
+#: constraint.cpp:766
+msgid ""
+"Bad selection for parallel / tangent constraint. This constraint can apply to:\n"
+"\n"
+" * two line segments (parallel)\n"
+" * a line segment and a normal (parallel)\n"
+" * two normals (parallel)\n"
+" * two line segments, arcs, or beziers, that share an endpoint (tangent)\n"
+msgstr ""
+"平行・正接拘束ができませんでした。次の要素に適用できます:\n"
+"\n"
+" * 2 つの線分 (平行)\n"
+" * 1 つの線分と 1 つの法線 (平行)\n"
+" * 2 つの法線 (平行)\n"
+" * 片方の端点を共有する 2 つの線分、円弧、もしくはベジェ曲線 (正接)\n"
+
+#: constraint.cpp:784
+msgid ""
+"Bad selection for perpendicular constraint. This constraint can apply to:\n"
+"\n"
+" * two line segments\n"
+" * a line segment and a normal\n"
+" * two normals\n"
+msgstr ""
+"垂線拘束ができませんでした。次の要素に適用できます:\n"
+"\n"
+" * 2 つの線分\n"
+" * 1 つの線分と 1 つの法線\n"
+" * 2 つの法線\n"
+
+#: constraint.cpp:799
+msgid ""
+"Bad selection for lock point where dragged constraint. This constraint can apply to:\n"
+"\n"
+" * a point\n"
+msgstr ""
+"点の位置の拘束 (点を他のオブジェクトに連動して動かさない) ができませんでした。次の要素に適用できま"
+"す:\n"
+"\n"
+" * 1 つの点\n"
+
+#: constraint.cpp:813 mouse.cpp:1158
+msgid "NEW COMMENT -- DOUBLE-CLICK TO EDIT"
+msgstr "(コメント - ダブルクリックで編集)"
+
+#: constraint.cpp:818
+msgid "click center of comment text"
+msgstr "コメントテキストの中心をクリックで指定"
+
+#: export.cpp:19
+msgid ""
+"No solid model present; draw one with extrudes and revolves, or use Export 2d View to export bare "
+"lines and curves."
+msgstr ""
+"ソリッドモデルがありません。押し出しや周回、回転を利用するか、平面への投影視点のエクスポートで直線"
+"や曲線をそのまま出力します。"
+
+#: export.cpp:61
+msgid ""
+"Bad selection for export section. Please select:\n"
+"\n"
+" * nothing, with an active workplane (workplane is section plane)\n"
+" * a face (section plane through face)\n"
+" * a point and two line segments (plane through point and parallel to lines)\n"
+msgstr ""
+"2D断面の出力ができませんでした。次のように選択してください:\n"
+"\n"
+" * なにも選択せず、作業平面がアクティブである (作業平面を断面とします)\n"
+" * フェイス (フェイスを通る断面を用います)\n"
+" * 1 つの点と 2 つの線分 (点を通り直線と平行する断面を用います)\n"
+
+#: export.cpp:818
+msgid "Active group mesh is empty; nothing to export."
+msgstr "アクティブなグループのメッシュは空です。エクスポートは行なわれませんでした。"
+
+#: exportvector.cpp:336
+msgid "freehand lines were replaced with continuous lines"
+msgstr "フリーハンドの線は実線に置き換えられました。"
+
+#: exportvector.cpp:338
+msgid "zigzag lines were replaced with continuous lines"
+msgstr "ジグザグ線は実線に置き換えられました。"
+
+#: exportvector.cpp:592
+msgid "Some aspects of the drawing have no DXF equivalent and were not exported:\n"
+msgstr "図面のうち一部の側面は、対応するものがDXFにないため、それらは出力されませんでした:\n"
+
+#: exportvector.cpp:838
+msgid "PDF page size exceeds 200 by 200 inches; many viewers may reject this file."
+msgstr "PDFのページ寸法が 200x200 インチを超えています。多くのPDFリーダーでは正常に読み込めません。"
+
+#: file.cpp:44 group.cpp:91
+msgctxt "group-name"
+msgid "sketch-in-plane"
+msgstr ""
+
+#: file.cpp:62
+msgctxt "group-name"
+msgid "#references"
+msgstr ""
+
+#: file.cpp:550
+msgid "The file is empty. It may be corrupt."
+msgstr "ファイルが空です。ファイルが破損している可能性があります。"
+
+#: file.cpp:555
+msgid "Unrecognized data in file. This file may be corrupt, or from a newer version of the program."
+msgstr ""
+"ファイルに処理できないデータが含まれています。ファイルが破損しているか、より新しいバージョンで作成"
+"されたファイルの可能性があります。"
+
+#: file.cpp:867
+msgctxt "title"
+msgid "Missing File"
+msgstr "不明ファイル"
+
+#: file.cpp:868
+#, c-format
+msgctxt "dialog"
+msgid "The linked file “%s” is not present."
+msgstr "リンクされたファイル \"%s\" が見つかりません。"
+
+#: file.cpp:870
+msgctxt "dialog"
+msgid ""
+"Do you want to locate it manually?\n"
+"\n"
+"If you decline, any geometry that depends on the missing file will be permanently removed."
+msgstr ""
+"手動で配置しますか?\n"
+"\n"
+"キャンセルした場合、不明なファイルに依存しているすべての寸法は削除されます。"
+
+#: file.cpp:873
+msgctxt "button"
+msgid "&Yes"
+msgstr "はい (&Y)"
+
+#: file.cpp:875
+msgctxt "button"
+msgid "&No"
+msgstr "いいえ (&N)"
+
+#: file.cpp:877 solvespace.cpp:611
+msgctxt "button"
+msgid "&Cancel"
+msgstr "キャンセル (&C)"
+
+#: graphicswin.cpp:41
+msgid "&File"
+msgstr "ファイル (&F)"
+
+#: graphicswin.cpp:42
+msgid "&New"
+msgstr "新規作成 (&N)"
+
+#: graphicswin.cpp:43
+msgid "&Open..."
+msgstr "開く... (&O)"
+
+#: graphicswin.cpp:44
+msgid "Open &Recent"
+msgstr "最近使用したファイル (&R)"
+
+#: graphicswin.cpp:45
+msgid "&Save"
+msgstr "保存 (&S)"
+
+#: graphicswin.cpp:46
+msgid "Save &As..."
+msgstr "名前を付けて保存... (&A)"
+
+#: graphicswin.cpp:48
+msgid "Export &Image..."
+msgstr "画像をエクスポート... (&I)"
+
+#: graphicswin.cpp:49
+msgid "Export 2d &View..."
+msgstr "平面への投影視点をエクスポート... (&V)"
+
+#: graphicswin.cpp:50
+msgid "Export 2d &Section..."
+msgstr "2D断面をエクスポート... (&S)"
+
+#: graphicswin.cpp:51
+msgid "Export 3d &Wireframe..."
+msgstr "三次元ワイヤーフレームをエクスポート... (&W)"
+
+#: graphicswin.cpp:52
+msgid "Export Triangle &Mesh..."
+msgstr "三角メッシュをエクスポート... (&M)"
+
+#: graphicswin.cpp:53
+msgid "Export &Surfaces..."
+msgstr "サーフェスをエクスポート... (&S)"
+
+#: graphicswin.cpp:54
+msgid "Im&port..."
+msgstr "インポート... (&I)"
+
+#: graphicswin.cpp:57
+msgid "E&xit"
+msgstr "終了 (&E)"
+
+#: graphicswin.cpp:60
+msgid "&Edit"
+msgstr "編集 (&E)"
+
+#: graphicswin.cpp:61
+msgid "&Undo"
+msgstr "取り消し (&U)"
+
+#: graphicswin.cpp:62
+msgid "&Redo"
+msgstr "やり直し (&R)"
+
+#: graphicswin.cpp:63
+msgid "Re&generate All"
+msgstr "すべて再生成 (&g)"
+
+#: graphicswin.cpp:65
+msgid "Snap Selection to &Grid"
+msgstr "選択したオブジェクトをグリッドに合わせる (&G)"
+
+#: graphicswin.cpp:66
+msgid "Rotate Imported &90°"
+msgstr "リンクした部品を90度回転する (&9)"
+
+#: graphicswin.cpp:68
+msgid "Cu&t"
+msgstr "切り取り (&t)"
+
+#: graphicswin.cpp:69
+msgid "&Copy"
+msgstr "コピー (&C)"
+
+#: graphicswin.cpp:70
+msgid "&Paste"
+msgstr "貼り付け (&P)"
+
+#: graphicswin.cpp:71
+msgid "Paste &Transformed..."
+msgstr "複製しながら貼り付け... (&T)"
+
+#: graphicswin.cpp:72
+msgid "&Delete"
+msgstr "削除 (&D)"
+
+#: graphicswin.cpp:74
+msgid "Select &Edge Chain"
+msgstr "接続しているエッジを選択"
+
+#: graphicswin.cpp:75
+msgid "Select &All"
+msgstr "すべてを選択 (&A)"
+
+#: graphicswin.cpp:76
+msgid "&Unselect All"
+msgstr "選択を解除 (&U)"
+
+#: graphicswin.cpp:78
+msgid "&Line Styles..."
+msgstr "線のスタイル... (&L)"
+
+#: graphicswin.cpp:79
+msgid "&View Projection..."
+msgstr "カメラの設定... (&V)"
+
+#: graphicswin.cpp:81
+msgid "Con&figuration..."
+msgstr "設定 (&f)"
+
+#: graphicswin.cpp:84
+msgid "&View"
+msgstr "表示 (&V)"
+
+#: graphicswin.cpp:85
+msgid "Zoom &In"
+msgstr "拡大 (&I)"
+
+#: graphicswin.cpp:86
+msgid "Zoom &Out"
+msgstr "縮小 (&O)"
+
+#: graphicswin.cpp:87
+msgid "Zoom To &Fit"
+msgstr "選択オブジェクトの最適表示"
+
+#: graphicswin.cpp:89
+msgid "Align View to &Workplane"
+msgstr "作業平面に合わせる (&W)"
+
+#: graphicswin.cpp:90
+msgid "Nearest &Ortho View"
+msgstr "近傍の正面図視点へ移動 (&O)"
+
+#: graphicswin.cpp:91
+msgid "Nearest &Isometric View"
+msgstr "近傍の等角図視点へ移動 (&I)"
+
+#: graphicswin.cpp:92
+msgid "&Center View At Point"
+msgstr "点を表示の中心にする"
+
+#: graphicswin.cpp:94
+msgid "Show Snap &Grid"
+msgstr "グリッドを表示 (&G)"
+
+#: graphicswin.cpp:95
+msgid "Darken Inactive Solids"
+msgstr "アクティブでないソリッドを暗くする"
+
+#: graphicswin.cpp:96
+msgid "Use &Perspective Projection"
+msgstr "透視投影で表示 (&P)"
+
+#: graphicswin.cpp:97
+msgid "Show E&xploded View"
+msgstr "分解立体図を表示 (&x)"
+
+#: graphicswin.cpp:98
+msgid "Dimension &Units"
+msgstr "表示単位の選択 (&U)"
+
+#: graphicswin.cpp:99
+msgid "Dimensions in &Millimeters"
+msgstr "ミリメートル (&M)"
+
+#: graphicswin.cpp:100
+msgid "Dimensions in M&eters"
+msgstr "メートル (&e)"
+
+#: graphicswin.cpp:101
+msgid "Dimensions in &Inches"
+msgstr "インチ (&I)"
+
+#: graphicswin.cpp:102
+msgid "Dimensions in &Feet and Inches"
+msgstr "フィート・インチ (&F)"
+
+#: graphicswin.cpp:104
+msgid "Show &Toolbar"
+msgstr "ツールバーを表示 (&T)"
+
+#: graphicswin.cpp:105
+msgid "Show Property Bro&wser"
+msgstr "プロパティブラウザを表示 (&w)"
+
+#: graphicswin.cpp:107
+msgid "&Full Screen"
+msgstr "全画面表示 (&F)"
+
+#: graphicswin.cpp:109
+msgid "&New Group"
+msgstr "グループ (&N)"
+
+#: graphicswin.cpp:110
+msgid "Sketch In &3d"
+msgstr "三次元空間内でスケッチを始める (&3)"
+
+#: graphicswin.cpp:111
+msgid "Sketch In New &Workplane"
+msgstr "新しい作業平面上でスケッチを始める (&W)"
+
+#: graphicswin.cpp:113
+msgid "Step &Translating"
+msgstr "直線上にコピー (&T)"
+
+#: graphicswin.cpp:114
+msgid "Step &Rotating"
+msgstr "円上にコピー (&R)"
+
+#: graphicswin.cpp:116
+msgid "E&xtrude"
+msgstr "押し出し (&x)"
+
+#: graphicswin.cpp:117
+msgid "&Helix"
+msgstr "周回 (&H)"
+
+#: graphicswin.cpp:118
+msgid "&Lathe"
+msgstr "螺旋 (&L)"
+
+#: graphicswin.cpp:119
+msgid "Re&volve"
+msgstr "回転 (&v)"
+
+#: graphicswin.cpp:121
+msgid "Link / Assemble..."
+msgstr "リンク・アセンブル..."
+
+#: graphicswin.cpp:122
+msgid "Link Recent"
+msgstr "最近使用したファイルとリンク"
+
+#: graphicswin.cpp:124
+msgid "&Sketch"
+msgstr "スケッチ (&S)"
+
+#: graphicswin.cpp:125
+msgid "In &Workplane"
+msgstr "作業平面上でスケッチする (&W)"
+
+#: graphicswin.cpp:126
+msgid "Anywhere In &3d"
+msgstr "三次元空間内でスケッチする (&3)"
+
+#: graphicswin.cpp:128
+msgid "Datum &Point"
+msgstr "点 (&P)"
+
+#: graphicswin.cpp:129
+msgid "&Workplane"
+msgstr "作業平面 (&W)"
+
+#: graphicswin.cpp:131
+msgid "Line &Segment"
+msgstr "線分 (&S)"
+
+#: graphicswin.cpp:132
+msgid "C&onstruction Line Segment"
+msgstr "補助線の線分 (&o)"
+
+#: graphicswin.cpp:133
+msgid "&Rectangle"
+msgstr "四角形 (&R)"
+
+#: graphicswin.cpp:134
+msgid "&Circle"
+msgstr "円 (&C)"
+
+#: graphicswin.cpp:135
+msgid "&Arc of a Circle"
+msgstr "円弧 (&A)"
+
+#: graphicswin.cpp:136
+msgid "&Bezier Cubic Spline"
+msgstr "ベジェ曲線 (&B)"
+
+#: graphicswin.cpp:138
+msgid "&Text in TrueType Font"
+msgstr "TrueTypeフォントのテキスト (&T)"
+
+#: graphicswin.cpp:139
+msgid "&Image"
+msgstr "画像 (&I)"
+
+#: graphicswin.cpp:141
+msgid "To&ggle Construction"
+msgstr "補助線との切り替え (&g)"
+
+#: graphicswin.cpp:142
+msgid "Tangent &Arc at Point"
+msgstr "フィレット"
+
+#: graphicswin.cpp:143
+msgid "Split Curves at &Intersection"
+msgstr "交差箇所で分割 (&I)"
+
+#: graphicswin.cpp:145
+msgid "&Constrain"
+msgstr "拘束 (&C)"
+
+#: graphicswin.cpp:146
+msgid "&Distance / Diameter"
+msgstr "距離・直径 (&D)"
+
+#: graphicswin.cpp:147
+msgid "Re&ference Dimension"
+msgstr "参照寸法 (&f)"
+
+#: graphicswin.cpp:148
+msgid "A&ngle"
+msgstr "角度 (&n)"
+
+#: graphicswin.cpp:149
+msgid "Reference An&gle"
+msgstr "参照角度 (&g)"
+
+#: graphicswin.cpp:150
+msgid "Other S&upplementary Angle"
+msgstr "補角 (&u)"
+
+#: graphicswin.cpp:151
+msgid "Toggle R&eference Dim"
+msgstr "拘束と参照を切り替え (&e)"
+
+#: graphicswin.cpp:153
+msgid "&Horizontal"
+msgstr "水平 (&H)"
+
+#: graphicswin.cpp:154
+msgid "&Vertical"
+msgstr "垂直 (&V)"
+
+#: graphicswin.cpp:156
+msgid "&On Point / Curve / Plane"
+msgstr "一致 (&O)"
+
+#: graphicswin.cpp:157
+msgid "E&qual Length / Radius / Angle"
+msgstr "長さ・半径・角度を等値にする (&q)"
+
+#: graphicswin.cpp:158
+msgid "Length / Arc Ra&tio"
+msgstr "長さ・円弧の比率 (&t)"
+
+#: graphicswin.cpp:159
+msgid "Length / Arc Diff&erence"
+msgstr "長さ・円弧の差 (&e)"
+
+#: graphicswin.cpp:160
+msgid "At &Midpoint"
+msgstr "中点 (&M)"
+
+#: graphicswin.cpp:161
+msgid "S&ymmetric"
+msgstr "対称 (&y)"
+
+#: graphicswin.cpp:162
+msgid "Para&llel / Tangent"
+msgstr "平行・正接 (&l)"
+
+#: graphicswin.cpp:163
+msgid "&Perpendicular"
+msgstr "垂線 (&P)"
+
+#: graphicswin.cpp:164
+msgid "Same Orient&ation"
+msgstr "同じ方向 (&a)"
+
+#: graphicswin.cpp:165
+msgid "Lock Point Where &Dragged"
+msgstr "点を他のオブジェクトに連動して動かさない (&D)"
+
+#: graphicswin.cpp:167
+msgid "Comment"
+msgstr "コメントを配置"
+
+#: graphicswin.cpp:169
+msgid "&Analyze"
+msgstr "解析 (&A)"
+
+#: graphicswin.cpp:170
+msgid "Measure &Volume"
+msgstr "体積を計測 (&V)"
+
+#: graphicswin.cpp:171
+msgid "Measure A&rea"
+msgstr "面積を計測 (&r)"
+
+#: graphicswin.cpp:172
+msgid "Measure &Perimeter"
+msgstr "外周長を計測 (&P)"
+
+#: graphicswin.cpp:173
+msgid "Show &Interfering Parts"
+msgstr "部品の干渉を強調表示 (&I)"
+
+#: graphicswin.cpp:174
+msgid "Show &Naked Edges"
+msgstr "浮いたエッジを強調表示 (&N)"
+
+#: graphicswin.cpp:175
+msgid "Show &Center of Mass"
+msgstr "重心を表示 (&C)"
+
+#: graphicswin.cpp:177
+msgid "Show &Underconstrained Points"
+msgstr "拘束されていない点を強調表示 (&U)"
+
+#: graphicswin.cpp:179
+msgid "&Trace Point"
+msgstr "点の位置の記録を開始 (&T)"
+
+#: graphicswin.cpp:180
+msgid "&Stop Tracing..."
+msgstr "記録を終了して保存する... (&S)"
+
+#: graphicswin.cpp:181
+msgid "Step &Dimension..."
+msgstr "寸法を段階的に変更... (&D)"
+
+#: graphicswin.cpp:183
+msgid "&Help"
+msgstr "ヘルプ (&H)"
+
+#: graphicswin.cpp:184
+msgid "&Language"
+msgstr "言語 (&L)"
+
+#: graphicswin.cpp:185
+msgid "&Website / Manual"
+msgstr "Webサイト・マニュアル"
+
+#: graphicswin.cpp:186
+msgid "&Go to GitHub commit"
+msgstr "GitHubの該当コミットへ移動 (&G)"
+
+#: graphicswin.cpp:188
+msgid "&About"
+msgstr "SolveSpaceについて (&A)"
+
+#: graphicswin.cpp:362
+msgid "(no recent files)"
+msgstr "(履歴なし)"
+
+#: graphicswin.cpp:370
+#, c-format
+msgid "File '%s' does not exist."
+msgstr "ファイル '%s' は存在しません。"
+
+#: graphicswin.cpp:737
+msgid "No workplane is active, so the grid will not appear."
+msgstr "作業平面がアクティブでないため、グリッドは表示されません。"
+
+#: graphicswin.cpp:752
+msgid ""
+"The perspective factor is set to zero, so the view will always be a parallel projection.\n"
+"\n"
+"For a perspective projection, modify the perspective factor in the configuration screen. A value "
+"around 0.3 is typical."
+msgstr ""
+"透視投影の係数が 0 になっているため、表示は常に平行投影になります。\n"
+"\n"
+"透視投影にするには、係数を configuration から変更してください。値は 0.3 くらいが一般的です。"
+
+#: graphicswin.cpp:837
+msgid "Select a point; this point will become the center of the view on screen."
+msgstr "スクリーンの中央とする点を指定してください。"
+
+#: graphicswin.cpp:1137
+msgid "No additional entities share endpoints with the selected entities."
+msgstr "選択した要素と端点を共有する要素は、これ以上ありません。"
+
+#: graphicswin.cpp:1155
+msgid ""
+"To use this command, select a point or other entity from an linked part, or make a link group the "
+"active group."
+msgstr ""
+"このコマンドを利用するには、リンクされた部品の要素や点を選択するか、リンクのグループをアクティブに"
+"してください。"
+
+#: graphicswin.cpp:1178
+msgid ""
+"No workplane is active. Activate a workplane (with Sketch -> In Workplane) to define the plane "
+"for the snap grid."
+msgstr ""
+"作業平面がアクティブではありません。グリッドの平面を指定するには作業平面をアクティブにしてくださ"
+"い (「スケッチ -> 作業平面上でスケッチする」)。"
+
+#: graphicswin.cpp:1185
+msgid ""
+"Can't snap these items to grid; select points, text comments, or constraints with a label. To "
+"snap a line, select its endpoints."
+msgstr ""
+"この要素をグリッドに合わせることはできません。点、コメントテキスト、もしくは値のある拘束を選択して"
+"ください。直線をグリッドに合わせるには、端点を選択してください。"
+
+#: graphicswin.cpp:1270
+msgid "No workplane selected. Activating default workplane for this group."
+msgstr ""
+"作業平面がアクティブではありません。現在のグループのデフォルトの作業平面をアクティブにします。"
+
+#: graphicswin.cpp:1273
+msgid ""
+"No workplane is selected, and the active group does not have a default workplane. Try selecting a "
+"workplane, or activating a sketch-in-new-workplane group."
+msgstr ""
+"作業平面がアクティブではありません。また、現在のグループにはデフォルトの作業平面がありません。作業"
+"平面を選択するか、新しく作業平面グループを作成してください。"
+
+#: graphicswin.cpp:1294
+msgid ""
+"Bad selection for tangent arc at point. Select a single point, or select nothing to set up arc "
+"parameters."
+msgstr ""
+"フィェットの作成には無効な選択です。ひとつの点が選択されていればフィレットを作成します。なにも選択"
+"されていなければ、フィレット作成時のパラメーターを設定できます。"
+
+#: graphicswin.cpp:1305
+msgid "click point on arc (draws anti-clockwise)"
+msgstr "円弧上の点をクリックで指定 (反時計回りに描きます)"
+
+#: graphicswin.cpp:1306
+msgid "click to place datum point"
+msgstr "点の位置をクリックで指定"
+
+#: graphicswin.cpp:1307
+msgid "click first point of line segment"
+msgstr "線分の最初の点をクリックで指定"
+
+#: graphicswin.cpp:1309
+msgid "click first point of construction line segment"
+msgstr "補助線の線分の最初の点をクリックで指定"
+
+#: graphicswin.cpp:1310
+msgid "click first point of cubic segment"
+msgstr "ベジェ曲線の最初の点をクリックで指定"
+
+#: graphicswin.cpp:1311
+msgid "click center of circle"
+msgstr "円の中央をクリックで指定"
+
+#: graphicswin.cpp:1312
+msgid "click origin of workplane"
+msgstr "作業平面の原点をクリックで指定"
+
+#: graphicswin.cpp:1313
+msgid "click one corner of rectangle"
+msgstr "四角形の角のひとつをクリックで指定"
+
+#: graphicswin.cpp:1314
+msgid "click top left of text"
+msgstr "テキストの右上をクリックで指定する"
+
+#: graphicswin.cpp:1320
+msgid "click top left of image"
+msgstr "画像の右上をクリックで指定する"
+
+#: graphicswin.cpp:1346
+msgid "No entities are selected. Select entities before trying to toggle their construction state."
+msgstr ""
+"スケッチ要素が選択されていません。拘束と参照を切り替えるには、最初に要素を選択してください。"
+
+#: group.cpp:86
+msgctxt "group-name"
+msgid "sketch-in-3d"
+msgstr ""
+
+#: group.cpp:150
+msgid ""
+"Bad selection for new sketch in workplane. This group can be created with:\n"
+"\n"
+" * a point (through the point, orthogonal to coordinate axes)\n"
+" * a point and two line segments (through the point, parallel to the lines)\n"
+" * a point and a normal (through the point, orthogonal to the normal)\n"
+" * a workplane (copy of the workplane)\n"
+msgstr ""
+"作業平面にスケッチを作成できませんでした。次のように選択してください。\n"
+"\n"
+" * 点 (点を通り、座標軸に直交する作業平面を作成します)\n"
+" * 点と、2 つの線分 (点を通り、直線に平行する作業平面を作成します)\n"
+" * 点と、法線 (点を通り、法線と直交する作業平面を作成します)\n"
+" * 作業平面 (あるいは作業平面のコピー)\n"
+
+#: group.cpp:166
+msgid ""
+"Activate a workplane (Sketch -> In Workplane) before extruding. The sketch will be extruded "
+"normal to the workplane."
+msgstr ""
+"押し出しを作成する前に、「スケッチ -> 作業平面上でスケッチする」で作業平面をアクティブにしてくださ"
+"い。作業平面の法線方向へ押し出しを作成します。"
+
+#: group.cpp:175
+msgctxt "group-name"
+msgid "extrude"
+msgstr ""
+
+#: group.cpp:180
+msgid "Lathe operation can only be applied to planar sketches."
+msgstr "周回操作は作業平面上にあるスケッチにのみ適用できます。"
+
+#: group.cpp:191
+msgid ""
+"Bad selection for new lathe group. This group can be created with:\n"
+"\n"
+" * a point and a line segment or normal (revolved about an axis parallel to line / normal, "
+"through point)\n"
+" * a line segment (revolved about line segment)\n"
+msgstr ""
+"周回を作成できませんでした。次のように選択してください。\n"
+"\n"
+" * 点と、線分もしくは法線 (点を通る回転軸を仮定し、直線・法線に平行に回転します)\n"
+" * 線分 (直線を回転軸とします)\n"
+
+#: group.cpp:201
+msgctxt "group-name"
+msgid "lathe"
+msgstr ""
+
+#: group.cpp:206
+msgid "Revolve operation can only be applied to planar sketches."
+msgstr "回転操作は作業平面上にあるスケッチにのみ適用できます。"
+
+#: group.cpp:217
+msgid ""
+"Bad selection for new revolve group. This group can be created with:\n"
+"\n"
+" * a point and a line segment or normal (revolved about an axis parallel to line / normal, "
+"through point)\n"
+" * a line segment (revolved about line segment)\n"
+msgstr ""
+"回転を作成できませんでした。次のように選択してください。\n"
+"\n"
+" * 点と、線分もしくは法線 (点を通る回転軸を仮定し、直線・法線に平行に回転します)\n"
+" * 線分 (直線を回転軸とします)\n"
+
+#: group.cpp:229
+msgctxt "group-name"
+msgid "revolve"
+msgstr ""
+
+#: group.cpp:234
+msgid "Helix operation can only be applied to planar sketches."
+msgstr "螺旋操作は作業平面上にあるスケッチにのみ適用できます。"
+
+#: group.cpp:245
+msgid ""
+"Bad selection for new helix group. This group can be created with:\n"
+"\n"
+" * a point and a line segment or normal (revolved about an axis parallel to line / normal, "
+"through point)\n"
+" * a line segment (revolved about line segment)\n"
+msgstr ""
+"螺旋を作成できませんでした。次のように選択してください。\n"
+"\n"
+" * 点と、線分もしくは法線 (点を通る回転軸を仮定し、直線・法線に平行に螺旋を作成します)\n"
+" * 線分 (直線を回転軸とします)\n"
+
+#: group.cpp:257
+msgctxt "group-name"
+msgid "helix"
+msgstr ""
+
+#: group.cpp:270
+msgid ""
+"Bad selection for new rotation. This group can be created with:\n"
+"\n"
+" * a point, while locked in workplane (rotate in plane, about that point)\n"
+" * a point and a line or a normal (rotate about an axis through the point, and parallel to "
+"line / normal)\n"
+msgstr ""
+"円上にコピーを作成できませんでした。次のように選択してください。\n"
+"\n"
+" * 作業平面上にある点 (作業平面上で、これを原点として周回します)\n"
+" * 点と、直線もしくは法線 (点を通る回転軸を仮定し、直線・法線と平行に周回します)\n"
+
+#: group.cpp:283
+msgctxt "group-name"
+msgid "rotate"
+msgstr ""
+
+#: group.cpp:294
+msgctxt "group-name"
+msgid "translate"
+msgstr ""
+
+#: group.cpp:416
+msgid "(unnamed)"
+msgstr "(名称未設定)"
+
+#: groupmesh.cpp:707
+msgid "not closed contour, or not all same style!"
+msgstr "閉じた輪郭でない、もしくは、輪郭を構成する要素のスタイルが同一ではありません。"
+
+#: groupmesh.cpp:720
+msgid "points not all coplanar!"
+msgstr "点が同一平面上にありません。"
+
+#: groupmesh.cpp:722
+msgid "contour is self-intersecting!"
+msgstr "輪郭線が自己交差しています。"
+
+#: groupmesh.cpp:724
+msgid "zero-length edge!"
+msgstr "長さが 0 のエッジです。"
+
+#: importmesh.cpp:136
+msgid "Text-formated STL files are not currently supported"
+msgstr "テキスト形式のSTLファイルは、いまのところサポートされていません。"
+
+#: modify.cpp:252
+msgid "Must be sketching in workplane to create tangent arc."
+msgstr "正接円弧をスケッチするには、作業平面上でスケッチしてください。"
+
+#: modify.cpp:299
+msgid ""
+"To create a tangent arc, select a point where two non-construction lines or circles in this group "
+"and workplane join."
+msgstr ""
+"正接円弧を作成するには、同じグループかつ同じ作業平面上にある、補助線ではない2つの直線もしくは円が"
+"交差している点を選択してください。"
+
+#: modify.cpp:386
+msgid ""
+"Couldn't round this corner. Try a smaller radius, or try creating the desired geometry by hand "
+"with tangency constraints."
+msgstr ""
+"この角をフィレットにできませんでした。もっと小さい半径を指定するか、手作業で拘束してください。"
+
+#: modify.cpp:595
+msgid "Couldn't split this entity; lines, circles, or cubics only."
+msgstr "この要素は分割できません。分割できるのは直線・円・ベジェ曲線のみです。"
+
+#: modify.cpp:622
+msgid "Must be sketching in workplane to split."
+msgstr "分割するには作業平面上でスケッチしてください。"
+
+#: modify.cpp:629
+msgid ""
+"Select two entities that intersect each other (e.g. two lines/circles/arcs or a line/circle/arc "
+"and a point)."
+msgstr "互いに交差している 2 つの要素を選択してください (直線・円・円弧・点)"
+
+#: modify.cpp:734
+msgid "Can't split; no intersection found."
+msgstr "交差箇所が見つからないため、分割できません。"
+
+#: mouse.cpp:557
+msgid "Assign to Style"
+msgstr "スタイルを割り当て"
+
+#: mouse.cpp:573
+msgid "No Style"
+msgstr "スタイル指定なし"
+
+#: mouse.cpp:576
+msgid "Newly Created Custom Style..."
+msgstr "スタイルを作成して割り当て..."
+
+#: mouse.cpp:583
+msgid "Group Info"
+msgstr "グループ情報"
+
+#: mouse.cpp:603
+msgid "Style Info"
+msgstr "スタイル情報"
+
+#: mouse.cpp:623
+msgid "Select Edge Chain"
+msgstr "接続しているエッジをまとめて選択"
+
+#: mouse.cpp:629
+msgid "Toggle Reference Dimension"
+msgstr "参照寸法と切り替え"
+
+#: mouse.cpp:635
+msgid "Other Supplementary Angle"
+msgstr "補角と切り替え"
+
+#: mouse.cpp:640
+msgid "Snap to Grid"
+msgstr "グリッドに沿わせる"
+
+#: mouse.cpp:649
+msgid "Remove Spline Point"
+msgstr "スプラインの制御点を削除"
+
+#: mouse.cpp:684
+msgid "Add Spline Point"
+msgstr "スプラインの制御点を追加"
+
+#: mouse.cpp:688
+msgid "Cannot add spline point: maximum number of points reached."
+msgstr "スプラインの制御点を追加できませんでした。点の個数の上限に達しています。"
+
+#: mouse.cpp:713
+msgid "Toggle Construction"
+msgstr "補助線との切り替え"
+
+#: mouse.cpp:729
+msgid "Delete Point-Coincident Constraint"
+msgstr "点の一致拘束を削除する"
+
+#: mouse.cpp:747
+msgid "Cut"
+msgstr "切り取り"
+
+#: mouse.cpp:749
+msgid "Copy"
+msgstr "コピー"
+
+#: mouse.cpp:753
+msgid "Select All"
+msgstr "すべてを選択"
+
+#: mouse.cpp:758
+msgid "Paste"
+msgstr "貼り付け"
+
+#: mouse.cpp:760
+msgid "Paste Transformed..."
+msgstr "複製しながら貼り付け..."
+
+#: mouse.cpp:765
+msgid "Delete"
+msgstr "削除"
+
+#: mouse.cpp:768
+msgid "Unselect All"
+msgstr "選択を解除"
+
+#: mouse.cpp:775
+msgid "Unselect Hovered"
+msgstr "カーソルを当てている要素の選択を解除"
+
+#: mouse.cpp:784
+msgid "Zoom to Fit"
+msgstr "選択オブジェクトの最適表示"
+
+#: mouse.cpp:986 mouse.cpp:1274
+msgid "click next point of line, or press Esc"
+msgstr "直線の次の点をクリックで指定、もしくはESCで終了"
+
+#: mouse.cpp:992
+msgid "Can't draw rectangle in 3d; first, activate a workplane with Sketch -> In Workplane."
+msgstr ""
+"三次元空間内に四角形はスケッチできません。「スケッチ -> 作業平面上でスケッチする」で作業平面をアク"
+"ティブにしてください。"
+
+#: mouse.cpp:1026
+msgid "click to place other corner of rectangle"
+msgstr "四角形のもう一つの角をクリックで指定"
+
+#: mouse.cpp:1047
+msgid "click to set radius"
+msgstr "半径をクリックで指定"
+
+#: mouse.cpp:1052
+msgid "Can't draw arc in 3d; first, activate a workplane with Sketch -> In Workplane."
+msgstr ""
+"三次元空間内に円弧はスケッチできません。「スケッチ -> 作業平面上でスケッチする」で作業平面をアク"
+"ティブにしてください。"
+
+#: mouse.cpp:1071
+msgid "click to place point"
+msgstr "点の位置をクリックで指定"
+
+#: mouse.cpp:1087
+msgid "click next point of cubic, or press Esc"
+msgstr "ベジェ曲線の次の点をクリックで指定、もしくはESCで終了"
+
+#: mouse.cpp:1092
+msgid "Sketching in a workplane already; sketch in 3d before creating new workplane."
+msgstr ""
+"すでに作業平面上でのスケッチをしています。新しい作業平面を作成する前に、三次元空間内でスケッチして"
+"ください。"
+
+#: mouse.cpp:1108
+msgid "Can't draw text in 3d; first, activate a workplane with Sketch -> In Workplane."
+msgstr ""
+"三次元空間内にテキストはスケッチできません。「スケッチ -> 作業平面上でスケッチする」で作業平面をア"
+"クティブにしてください。"
+
+#: mouse.cpp:1125
+msgid "click to place bottom right of text"
+msgstr "テキストの左下の位置をクリックで指定"
+
+#: mouse.cpp:1131
+msgid "Can't draw image in 3d; first, activate a workplane with Sketch -> In Workplane."
+msgstr ""
+"三次元空間内に画像はスケッチできません。「スケッチ -> 作業平面上でスケッチする」で作業平面をアク"
+"ティブにしてください。"
+
+#: platform/gui.cpp:85 platform/gui.cpp:90 solvespace.cpp:553
+msgctxt "file-type"
+msgid "SolveSpace models"
+msgstr "SolveSpaceモデル"
+
+#: platform/gui.cpp:89
+msgctxt "file-type"
+msgid "ALL"
+msgstr "(すべてのファイル)"
+
+#: platform/gui.cpp:91
+msgctxt "file-type"
+msgid "IDF circuit board"
+msgstr "IDF 電子基板"
+
+#: platform/gui.cpp:92
+msgctxt "file-type"
+msgid "STL triangle mesh"
+msgstr "STL 三角メッシュ"
+
+#: platform/gui.cpp:96
+msgctxt "file-type"
+msgid "PNG image"
+msgstr "PNG 画像"
+
+#: platform/gui.cpp:100
+msgctxt "file-type"
+msgid "STL mesh"
+msgstr "STL メッシュ"
+
+#: platform/gui.cpp:101
+msgctxt "file-type"
+msgid "Wavefront OBJ mesh"
+msgstr "Wavefront OBJ メッシュ"
+
+#: platform/gui.cpp:102
+msgctxt "file-type"
+msgid "Three.js-compatible mesh, with viewer"
+msgstr "Three.js 互換のメッシュ (ビューワー付)"
+
+#: platform/gui.cpp:103
+msgctxt "file-type"
+msgid "Three.js-compatible mesh, mesh only"
+msgstr "Three.js 互換のメッシュ (メッシュのみ)"
+
+#: platform/gui.cpp:104
+msgctxt "file-type"
+msgid "VRML text file"
+msgstr "VRML テキストファイル"
+
+#: platform/gui.cpp:108 platform/gui.cpp:115 platform/gui.cpp:122
+msgctxt "file-type"
+msgid "STEP file"
+msgstr "STEP ファイル"
+
+#: platform/gui.cpp:112
+msgctxt "file-type"
+msgid "PDF file"
+msgstr "PDF ファイル"
+
+#: platform/gui.cpp:113
+msgctxt "file-type"
+msgid "Encapsulated PostScript"
+msgstr "EPS ファイル (Encapsulated PostScript)"
+
+#: platform/gui.cpp:114
+msgctxt "file-type"
+msgid "Scalable Vector Graphics"
+msgstr "SVG ファイル"
+
+#: platform/gui.cpp:116 platform/gui.cpp:123
+msgctxt "file-type"
+msgid "DXF file (AutoCAD 2007)"
+msgstr "DXF ファイル (AutoDesk 2007)"
+
+#: platform/gui.cpp:117
+msgctxt "file-type"
+msgid "HPGL file"
+msgstr "HPG ファイル"
+
+#: platform/gui.cpp:118
+msgctxt "file-type"
+msgid "G Code"
+msgstr "G コード"
+
+#: platform/gui.cpp:127
+msgctxt "file-type"
+msgid "AutoCAD DXF and DWG files"
+msgstr "AudoCAD DXF/DWG ファイル"
+
+#: platform/gui.cpp:131
+msgctxt "file-type"
+msgid "Comma-separated values"
+msgstr "CSV ファイル (Comma-separated values)"
+
+#: platform/guigtk.cpp:1382 platform/guimac.mm:1509 platform/guiwin.cpp:1641
+msgid "untitled"
+msgstr "名称未設定"
+
+#: platform/guigtk.cpp:1393 platform/guigtk.cpp:1426 platform/guimac.mm:1467
+#: platform/guiwin.cpp:1639
+msgctxt "title"
+msgid "Save File"
+msgstr "ファイルを保存"
+
+#: platform/guigtk.cpp:1394 platform/guigtk.cpp:1427 platform/guimac.mm:1450
+#: platform/guiwin.cpp:1645
+msgctxt "title"
+msgid "Open File"
+msgstr "ファイルを開く"
+
+#: platform/guigtk.cpp:1397 platform/guigtk.cpp:1433
+msgctxt "button"
+msgid "_Cancel"
+msgstr "キャンセル (_C)"
+
+#: platform/guigtk.cpp:1398 platform/guigtk.cpp:1431
+msgctxt "button"
+msgid "_Save"
+msgstr "保存 (_S)"
+
+#: platform/guigtk.cpp:1399 platform/guigtk.cpp:1432
+msgctxt "button"
+msgid "_Open"
+msgstr "開く (_O)"
+
+#: solvespace.cpp:171
+msgctxt "title"
+msgid "Autosave Available"
+msgstr "オートセーブファイルがあります"
+
+#: solvespace.cpp:172
+msgctxt "dialog"
+msgid "An autosave file is available for this sketch."
+msgstr "このスケッチにはオートセーブファイルがあります。"
+
+#: solvespace.cpp:173
+msgctxt "dialog"
+msgid "Do you want to load the autosave file instead?"
+msgstr "代わりにオートセーブファイル読み込みますか?"
+
+#: solvespace.cpp:174
+msgctxt "button"
+msgid "&Load autosave"
+msgstr "オートセーブファイルを読み込む (&L)"
+
+#: solvespace.cpp:176
+msgctxt "button"
+msgid "Do&n't Load"
+msgstr "読み込まない (&n)"
+
+#: solvespace.cpp:599
+msgctxt "title"
+msgid "Modified File"
+msgstr "編集済みファイル"
+
+#: solvespace.cpp:601
+#, c-format
+msgctxt "dialog"
+msgid "Do you want to save the changes you made to the sketch “%s”?"
+msgstr "スケッチ \"%s\" への変更を保存しますか?"
+
+#: solvespace.cpp:604
+msgctxt "dialog"
+msgid "Do you want to save the changes you made to the new sketch?"
+msgstr "新規スケッチへの変更を保存しますか?"
+
+#: solvespace.cpp:607
+msgctxt "dialog"
+msgid "Your changes will be lost if you don't save them."
+msgstr "保存しなければ変更は失われます。"
+
+#: solvespace.cpp:608
+msgctxt "button"
+msgid "&Save"
+msgstr "保存する (&S)"
+
+#: solvespace.cpp:610
+msgctxt "button"
+msgid "Do&n't Save"
+msgstr "保存しない (&n)"
+
+#: solvespace.cpp:631
+msgctxt "title"
+msgid "(new sketch)"
+msgstr "(新規スケッチ)"
+
+#: solvespace.cpp:638
+msgctxt "title"
+msgid "Property Browser"
+msgstr "プロパティブラウザ"
+
+#: solvespace.cpp:700
+msgid ""
+"Constraints are currently shown, and will be exported in the toolpath. This is probably not what "
+"you want; hide them by clicking the link at the top of the text window."
+msgstr ""
+"表示されている拘束はツールパスとしてエクスポートされます。これはあなたが期待した動作ではないかもし"
+"れません。プロパティブラウザの上部にあるボタンで非表示にできます。"
+
+#: solvespace.cpp:772
+#, c-format
+msgid "Can't identify file type from file extension of filename '%s'; try .dxf or .dwg."
+msgstr ""
+"拡張子からファイル形式を特定できませんでした ( '%s' )。 \".dxf\" や \".dwg\" を試してください。"
+
+#: solvespace.cpp:824
+msgid "Constraint must have a label, and must not be a reference dimension."
+msgstr "参照寸法ではなく、かつ、値のある拘束を選択してください。"
+
+#: solvespace.cpp:828
+msgid "Bad selection for step dimension; select a constraint."
+msgstr "寸法の段階的変更には無効な選択です。値のある拘束を選択してください。"
+
+#: solvespace.cpp:852
+msgid "The assembly does not interfere, good."
+msgstr "部品の干渉はありませんでした。"
+
+#: solvespace.cpp:868
+#, c-format
+msgid ""
+"The volume of the solid model is:\n"
+"\n"
+" %s"
+msgstr ""
+"ソリッドモデルの容積:\n"
+"\n"
+" %s"
+
+#: solvespace.cpp:877
+#, c-format
+msgid ""
+"\n"
+"The volume of current group mesh is:\n"
+"\n"
+" %s"
+msgstr ""
+"\n"
+"現在のグループのメッシュの容積:\n"
+"\n"
+" %s"
+
+#: solvespace.cpp:882
+msgid ""
+"\n"
+"\n"
+"Curved surfaces have been approximated as triangles.\n"
+"This introduces error, typically of around 1%."
+msgstr ""
+"\n"
+"\n"
+"曲面はトライアングルに近似します。\n"
+"これにより通常、約 1% の誤差が発生します。"
+
+#: solvespace.cpp:897
+#, c-format
+msgid ""
+"The surface area of the selected faces is:\n"
+"\n"
+" %s\n"
+"\n"
+"Curves have been approximated as piecewise linear.\n"
+"This introduces error, typically of around 1%%."
+msgstr ""
+"選択した面のファーフェスの面積:\n"
+"\n"
+" %s\n"
+"\n"
+"曲線は区分線形に近似します。\n"
+"これにより通常、約 1%% の誤差が発生します。"
+
+#: solvespace.cpp:906
+msgid ""
+"This group does not contain a correctly-formed 2d closed area. It is open, not coplanar, or self-"
+"intersecting."
+msgstr ""
+"このグループは正常に構成された二次元の閉じた領域が含まれていません。領域が開いているか、同一平面上"
+"にないか、自己交差しています。"
+
+#: solvespace.cpp:918
+#, c-format
+msgid ""
+"The area of the region sketched in this group is:\n"
+"\n"
+" %s\n"
+"\n"
+"Curves have been approximated as piecewise linear.\n"
+"This introduces error, typically of around 1%%."
+msgstr ""
+"このグループ内のスケッチされた領域の面積:\n"
+"\n"
+" %s\n"
+"\n"
+"曲線は区分線形に近似します。\n"
+"これにより通常、約 1%% の誤差が発生します。"
+
+#: solvespace.cpp:938
+#, c-format
+msgid ""
+"The total length of the selected entities is:\n"
+"\n"
+" %s\n"
+"\n"
+"Curves have been approximated as piecewise linear.\n"
+"This introduces error, typically of around 1%%."
+msgstr ""
+"選択された要素の合計の長さ:\n"
+"\n"
+" %s\n"
+"\n"
+"曲線は区分線形に近似します。\n"
+"これにより通常、約 1%% の誤差が発生します。"
+
+#: solvespace.cpp:944
+msgid "Bad selection for perimeter; select line segments, arcs, and curves."
+msgstr "外周長の計測には無効な選択です。線分、円弧、もしくは曲線を選択してください。"
+
+#: solvespace.cpp:960
+msgid "Bad selection for trace; select a single point."
+msgstr "点の位置の記録には無効な選択です。ひとつの点を選択してください。"
+
+#: solvespace.cpp:987
+#, c-format
+msgid "Couldn't write to '%s'"
+msgstr "'%s' へ書き込めませんでした。"
+
+#: solvespace.cpp:1017
+msgid "The mesh is self-intersecting (NOT okay, invalid)."
+msgstr "メッシュが自身と交差しています (警告)"
+
+#: solvespace.cpp:1018
+msgid "The mesh is not self-intersecting (okay, valid)."
+msgstr "メッシュは自己交差していません (適切)"
+
+#: solvespace.cpp:1020
+msgid "The mesh has naked edges (NOT okay, invalid)."
+msgstr "メッシュには浮いたエッジが含まれています (警告)"
+
+#: solvespace.cpp:1021
+msgid "The mesh is watertight (okay, valid)."
+msgstr "メッシュには隙間はありません (適切)"
+
+#: solvespace.cpp:1024
+#, c-format
+msgid ""
+"\n"
+"\n"
+"The model contains %d triangles, from %d surfaces."
+msgstr ""
+"\n"
+"\n"
+"モデルには %d 個のトライアングルがあり、これが %d 個のサーフェスを構成します。"
+
+#: solvespace.cpp:1028
+#, c-format
+msgid ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"Zero problematic edges, good.%s"
+msgstr ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"問題のあるエッジは見つかりませんでした。%s"
+
+#: solvespace.cpp:1031
+#, c-format
+msgid ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"%d problematic edges, bad.%s"
+msgstr ""
+"%s\n"
+"\n"
+"%s\n"
+"\n"
+"%d 個の問題のあるエッジが見つかりました。%s"
+
+#: solvespace.cpp:1044
+#, c-format
+msgid ""
+"This is SolveSpace version %s.\n"
+"\n"
+"For more information, see http://solvespace.com/\n"
+"\n"
+"SolveSpace is free software: you are free to modify\n"
+"and/or redistribute it under the terms of the GNU\n"
+"General Public License (GPL) version 3 or later.\n"
+"\n"
+"There is NO WARRANTY, to the extent permitted by\n"
+"law. For details, visit http://gnu.org/licenses/\n"
+"\n"
+"© 2008-%d Jonathan Westhues and other authors.\n"
+msgstr ""
+
+#: style.cpp:185
+msgid ""
+"Can't assign style to an entity that's derived from another entity; try assigning a style to this "
+"entity's parent."
+msgstr ""
+"他の要素から派生した要素には個別のスタイルを割り当てることはできません。親要素にスタイルを割り当て"
+"てください。"
+
+#: style.cpp:735
+msgid "Style name cannot be empty"
+msgstr "スタイル名は空にできません"
+
+#: textscreens.cpp:791
+msgid "Can't repeat fewer than 1 time."
+msgstr "繰り返しは 1 以上の回数を指定してください。"
+
+#: textscreens.cpp:795
+msgid "Can't repeat more than 999 times."
+msgstr "999回を超えて繰り返すことはできません。"
+
+#: textscreens.cpp:820
+msgid "Group name cannot be empty"
+msgstr "グループ名は空にできません"
+
+#: textscreens.cpp:872
+msgid "Opacity must be between zero and one."
+msgstr "透明度は0から1の間で指定してください。"
+
+#: textscreens.cpp:907
+msgid "Radius cannot be zero or negative."
+msgstr "半径は0もしくは負の値にできません。"
+
+#: toolbar.cpp:18
+msgid "Sketch line segment"
+msgstr "線分をスケッチ"
+
+#: toolbar.cpp:20
+msgid "Sketch rectangle"
+msgstr "四角形をスケッチ"
+
+#: toolbar.cpp:22
+msgid "Sketch circle"
+msgstr "円をスケッチ"
+
+#: toolbar.cpp:24
+msgid "Sketch arc of a circle"
+msgstr "円弧をスケッチ"
+
+#: toolbar.cpp:26
+msgid "Sketch curves from text in a TrueType font"
+msgstr "TrueTypeフォントのテキストからアウトラインをスケッチ"
+
+#: toolbar.cpp:28
+msgid "Sketch image from a file"
+msgstr "ファイルから画像を取り込む"
+
+#: toolbar.cpp:30
+msgid "Create tangent arc at selected point"
+msgstr "選択された点でフィレットを作る"
+
+#: toolbar.cpp:32
+msgid "Sketch cubic Bezier spline"
+msgstr "ベジェ曲線をスケッチ"
+
+#: toolbar.cpp:34
+msgid "Sketch datum point"
+msgstr "点をスケッチ"
+
+#: toolbar.cpp:36
+msgid "Toggle construction"
+msgstr "補助線と切り替え"
+
+#: toolbar.cpp:38
+msgid "Split lines / curves where they intersect"
+msgstr "直線・曲線を交差箇所で分割"
+
+#: toolbar.cpp:42
+msgid "Constrain distance / diameter / length"
+msgstr "距離・直径・長さを拘束"
+
+#: toolbar.cpp:44
+msgid "Constrain angle"
+msgstr "角度を拘束"
+
+#: toolbar.cpp:46
+msgid "Constrain to be horizontal"
+msgstr "水平に拘束"
+
+#: toolbar.cpp:48
+msgid "Constrain to be vertical"
+msgstr "垂直に拘束"
+
+#: toolbar.cpp:50
+msgid "Constrain to be parallel or tangent"
+msgstr "平行もしくは正接で拘束"
+
+#: toolbar.cpp:52
+msgid "Constrain to be perpendicular"
+msgstr "垂線で拘束"
+
+#: toolbar.cpp:54
+msgid "Constrain point on line / curve / plane / point"
+msgstr "点を線・曲線・面・点の上に拘束"
+
+#: toolbar.cpp:56
+msgid "Constrain symmetric"
+msgstr "対称に拘束"
+
+#: toolbar.cpp:58
+msgid "Constrain equal length / radius / angle"
+msgstr "長さ・半径・角度を等値に拘束"
+
+#: toolbar.cpp:60
+msgid "Constrain normals in same orientation"
+msgstr "法線を同じ方向に拘束"
+
+#: toolbar.cpp:62
+msgid "Other supplementary angle"
+msgstr "角度の拘束を補角へ移動"
+
+#: toolbar.cpp:64
+msgid "Toggle reference dimension"
+msgstr "拘束と参照寸法を切り替え"
+
+#: toolbar.cpp:68
+msgid "New group extruding active sketch"
+msgstr "押し出しを作成"
+
+#: toolbar.cpp:70
+msgid "New group rotating active sketch"
+msgstr "周回を作成"
+
+#: toolbar.cpp:72
+msgid "New group helix from active sketch"
+msgstr "螺旋を作成"
+
+#: toolbar.cpp:74
+msgid "New group revolve active sketch"
+msgstr "回転を作成"
+
+#: toolbar.cpp:76
+msgid "New group step and repeat rotating"
+msgstr "円上にコピーを作成"
+
+#: toolbar.cpp:78
+msgid "New group step and repeat translating"
+msgstr "直線上にコピーを作成"
+
+#: toolbar.cpp:80
+msgid "New group in new workplane (thru given entities)"
+msgstr "選択した要素で定義される作業平面でスケッチを開始"
+
+#: toolbar.cpp:82
+msgid "New group in 3d"
+msgstr "三次元空間内でスケッチを開始"
+
+#: toolbar.cpp:84
+msgid "New group linking / assembling file"
+msgstr "他のファイルをリンクする"
+
+#: toolbar.cpp:88
+msgid "Nearest isometric view"
+msgstr "近傍の等角図視点"
+
+#: toolbar.cpp:90
+msgid "Align view to active workplane"
+msgstr "アクティブな作業平面に視点を合わせる"
+
+#: util.cpp:165
+msgctxt "title"
+msgid "Error"
+msgstr "エラー"
+
+#: util.cpp:165
+msgctxt "title"
+msgid "Message"
+msgstr "メッセージ"
+
+#: util.cpp:170
+msgctxt "button"
+msgid "&OK"
+msgstr "OK (&O)"
+
+#: view.cpp:127
+msgid "Scale cannot be zero or negative."
+msgstr "縮尺は 0 や負の値にはできません。"
+
+#: view.cpp:139 view.cpp:148
+msgid "Bad format: specify x, y, z"
+msgstr "書式が誤っています。\"x, y, z\" で指定してください。"
diff --git a/res/locales/ru_RU.po b/res/locales/ru_RU.po
index bf9bbfbb..06cbbdf5 100644
--- a/res/locales/ru_RU.po
+++ b/res/locales/ru_RU.po
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: SolveSpace 3.0\n"
"Report-Msgid-Bugs-To: whitequark@whitequark.org\n"
"POT-Creation-Date: 2022-02-01 16:24+0200\n"
-"PO-Revision-Date: 2021-10-04 15:33+0300\n"
-"Last-Translator: Olesya Gerasimenko \n"
+"PO-Revision-Date: 2022-11-05 19:37+0200\n"
+"Last-Translator: ruevs Olesya Gerasimenko \n"
"Language-Team: Basealt Translation Team\n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
@@ -936,7 +936,7 @@ msgstr "Тело В&ращения"
#: graphicswin.cpp:119
msgid "Re&volve"
-msgstr "Тело В&ращения"
+msgstr "Тело В&ращения на угол"
#: graphicswin.cpp:121
msgid "Link / Assemble..."
@@ -1350,9 +1350,9 @@ msgstr ""
"Группа может быть создана, используя в качестве выделения следующие "
"примитивы:\n"
"\n"
-" * точку и отрезок / координатный базис (нормаль) (тело вращения вокруг "
+" * точку и отрезок / координатный базис (нормаль) (вращение вокруг "
"оси, проходящей через точку и параллельной отрезку / нормали)\n"
-" * отрезок (тело вращения вокруг оси, проходящей через отрезок)\n"
+" * отрезок (вращение вокруг оси, проходящей через отрезок)\n"
"\n"
#: group.cpp:201
@@ -1363,7 +1363,7 @@ msgstr "тело-вращения"
#: group.cpp:206
msgid "Revolve operation can only be applied to planar sketches."
msgstr ""
-"Операция создания тела вращения может быть применена только к плоским "
+"Операция создания тела вращения на угол может быть применена только к плоским "
"эскизам."
#: group.cpp:217
@@ -1374,19 +1374,19 @@ msgid ""
"to line / normal, through point)\n"
" * a line segment (revolved about line segment)\n"
msgstr ""
-"Неправильное выделение для создания группы тела вращения. \n"
+"Неправильное выделение для создания группы тела вращения на угол. \n"
"Группа может быть создана, используя в качестве выделения следующие "
"примитивы:\n"
"\n"
-" * точку и отрезок / координатный базис (нормаль) (тело вращения вокруг "
+" * точку и отрезок / координатный базис (нормаль) (вращение вокруг "
"оси, проходящей через точку и параллельной отрезку / нормали)\n"
-" * отрезок (тело вращения вокруг оси, проходящей через отрезок)\n"
+" * отрезок (вращение вокруг оси, проходящей через отрезок)\n"
"\n"
#: group.cpp:229
msgctxt "group-name"
msgid "revolve"
-msgstr "тело-вращения"
+msgstr "тело-вращения-на-угол"
#: group.cpp:234
msgid "Helix operation can only be applied to planar sketches."
@@ -2219,7 +2219,7 @@ msgstr "Создать группу выдавливания текущего э
#: toolbar.cpp:70
msgid "New group rotating active sketch"
-msgstr "Создать группу вращения текущего эскиза"
+msgstr "Создать группу тела вращения текущего эскиза"
#: toolbar.cpp:72
msgid "New group helix from active sketch"
@@ -2227,7 +2227,7 @@ msgstr "Создать группу тела выдавливания по ви
#: toolbar.cpp:74
msgid "New group revolve active sketch"
-msgstr "Создать группу тела вращения из текущего эскиза"
+msgstr "Создать группу тела вращения на угол из текущего эскиза"
#: toolbar.cpp:76
msgid "New group step and repeat rotating"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a6af09a9..1f21e16a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -82,7 +82,9 @@ target_compile_definitions(slvs
PRIVATE -DLIBRARY)
target_include_directories(slvs
- PUBLIC ${CMAKE_SOURCE_DIR}/include)
+ PUBLIC
+ ${CMAKE_SOURCE_DIR}/include
+ ${EIGEN3_INCLUDE_DIRS})
target_link_libraries(slvs PRIVATE slvs_deps)
@@ -101,7 +103,8 @@ endif()
set(every_platform_SOURCES
platform/guiwin.cpp
platform/guigtk.cpp
- platform/guimac.mm)
+ platform/guimac.mm
+ platform/guihtml.cpp)
# solvespace library
@@ -166,6 +169,7 @@ add_library(solvespace-core STATIC
srf/merge.cpp
srf/ratpoly.cpp
srf/raycast.cpp
+ srf/shell.cpp
srf/surface.cpp
srf/surfinter.cpp
srf/triangulate.cpp)
@@ -300,6 +304,56 @@ if(ENABLE_GUI)
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.solvespace"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
+ elseif(EMSCRIPTEN)
+ set(SHELL ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/emshell.html)
+ set(LINK_FLAGS
+ --bind --shell-file ${SHELL}
+ --no-heap-copy -s ALLOW_MEMORY_GROWTH=1 -s WASM=1 -s ASYNCIFY=1
+ -s DYNCALLS=1 -s ASSERTIONS=1
+ -s TOTAL_STACK=33554432 -s TOTAL_MEMORY=134217728)
+
+ get_target_property(resource_names resources NAMES)
+ foreach(resource ${resource_names})
+ list(APPEND LINK_FLAGS --preload-file ${resource})
+ endforeach()
+
+ if(CMAKE_BUILD_TYPE STREQUAL Debug)
+ list(APPEND LINK_FLAGS
+ --emrun --emit-symbol-map
+ -s DEMANGLE_SUPPORT=1
+ -s SAFE_HEAP=1)
+ endif()
+
+ target_sources(solvespace PRIVATE
+ platform/guihtml.cpp)
+
+ string(REPLACE ";" " " LINK_FLAGS "${LINK_FLAGS}")
+ set_target_properties(solvespace PROPERTIES
+ LINK_FLAGS "${LINK_FLAGS}")
+ set_source_files_properties(platform/guihtml.cpp PROPERTIES
+ OBJECT_DEPENDS ${SHELL})
+
+ add_custom_command(
+ TARGET solvespace POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.css
+ ${EXECUTABLE_OUTPUT_PATH}/solvespaceui.css
+ COMMENT "Copying UI stylesheet"
+ VERBATIM)
+ add_custom_command(
+ TARGET solvespace POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.js
+ ${EXECUTABLE_OUTPUT_PATH}/solvespaceui.js
+ COMMENT "Copying UI script solvespaceui.js"
+ VERBATIM)
+ add_custom_command(
+ TARGET solvespace POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/filemanagerui.js
+ ${EXECUTABLE_OUTPUT_PATH}/filemanagerui.js
+ COMMENT "Copying UI script filemanagerui.sj"
+ VERBATIM)
else()
target_sources(solvespace PRIVATE
platform/guigtk.cpp)
@@ -335,7 +389,8 @@ target_compile_definitions(solvespace-headless
PRIVATE HEADLESS)
target_include_directories(solvespace-headless
- INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
+ INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
+ PUBLIC ${EIGEN3_INCLUDE_DIRS})
target_link_libraries(solvespace-headless
PRIVATE
@@ -363,7 +418,7 @@ endif()
# solvespace unix package
-if(NOT (WIN32 OR APPLE))
+if(NOT (WIN32 OR APPLE OR EMSCRIPTEN))
if(ENABLE_GUI)
install(TARGETS solvespace
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/constraint.cpp b/src/constraint.cpp
index 6a25c265..fbea9e27 100644
--- a/src/constraint.cpp
+++ b/src/constraint.cpp
@@ -721,7 +721,11 @@ void Constraint::MenuConstrain(Command id) {
}
case Command::PARALLEL:
- if(gs.vectors == 2 && gs.n == 2) {
+ if(gs.faces == 2 && gs.n == 2) {
+ c.type = Type::PARALLEL;
+ c.entityA = gs.face[0];
+ c.entityB = gs.face[1];
+ } else if(gs.vectors == 2 && gs.n == 2) {
c.type = Type::PARALLEL;
c.entityA = gs.vector[0];
c.entityB = gs.vector[1];
@@ -765,6 +769,7 @@ void Constraint::MenuConstrain(Command id) {
} else {
Error(_("Bad selection for parallel / tangent constraint. This "
"constraint can apply to:\n\n"
+ " * two faces\n"
" * two line segments (parallel)\n"
" * a line segment and a normal (parallel)\n"
" * two normals (parallel)\n"
@@ -776,13 +781,18 @@ void Constraint::MenuConstrain(Command id) {
break;
case Command::PERPENDICULAR:
- if(gs.vectors == 2 && gs.n == 2) {
+ if(gs.faces == 2 && gs.n == 2) {
+ c.type = Type::PERPENDICULAR;
+ c.entityA = gs.face[0];
+ c.entityB = gs.face[1];
+ } else if(gs.vectors == 2 && gs.n == 2) {
c.type = Type::PERPENDICULAR;
c.entityA = gs.vector[0];
c.entityB = gs.vector[1];
} else {
Error(_("Bad selection for perpendicular constraint. This "
"constraint can apply to:\n\n"
+ " * two faces\n"
" * two line segments\n"
" * a line segment and a normal\n"
" * two normals\n"));
diff --git a/src/describescreen.cpp b/src/describescreen.cpp
index d2f94503..db527493 100644
--- a/src/describescreen.cpp
+++ b/src/describescreen.cpp
@@ -89,6 +89,7 @@ void TextWindow::DescribeSelection() {
case Entity::Type::POINT_N_ROT_TRANS:
case Entity::Type::POINT_N_COPY:
case Entity::Type::POINT_N_ROT_AA:
+ case Entity::Type::POINT_N_ROT_AXIS_TRANS:
p = e->PointGetNum();
Printf(false, "%FtPOINT%E at " PT_AS_STR, COSTR(e, p));
break;
@@ -167,10 +168,11 @@ void TextWindow::DescribeSelection() {
case Entity::Type::CIRCLE: {
Printf(false, "%FtCIRCLE%E");
p = SK.GetEntity(e->point[0])->PointGetNum();
- Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p));
+ Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p));
double r = e->CircleGetRadiusNum();
- Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).c_str());
- Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str());
+ Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).c_str());
+ Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str());
+ Printf(false, " circumference = %Fi%s", SS.MmToString(2*M_PI*r).c_str());
break;
}
case Entity::Type::FACE_NORMAL_PT:
@@ -178,7 +180,9 @@ void TextWindow::DescribeSelection() {
case Entity::Type::FACE_N_ROT_TRANS:
case Entity::Type::FACE_N_ROT_AA:
case Entity::Type::FACE_N_TRANS:
- Printf(false, "%FtPLANE FACE%E");
+ case Entity::Type::FACE_ROT_NORMAL_PT:
+ case Entity::Type::FACE_N_ROT_AXIS_TRANS:
+ Printf(false, "%FtPLANE FACE%E");
p = e->FaceGetNormalNum();
Printf(true, " normal = " PT_AS_NUM, CO(p));
p = e->FaceGetPointNum();
@@ -424,14 +428,19 @@ void TextWindow::DescribeSelection() {
double d = (p1.Minus(p0)).Dot(n0);
Printf(true, " distance = %Fi%s", SS.MmToString(d).c_str());
}
- } else if(gs.n == 0 && gs.stylables > 0) {
- Printf(false, "%FtSELECTED:%E comment text");
} else if(gs.n == 0 && gs.constraints == 1) {
Constraint *c = SK.GetConstraint(gs.constraint[0]);
const std::string &desc = c->DescriptionString().c_str();
if(c->type == Constraint::Type::COMMENT) {
Printf(false, "%FtCOMMENT%E %s", desc.c_str());
+ if(c->ptA != Entity::NO_ENTITY) {
+ Vector p = SK.GetEntity(c->ptA)->PointGetNum();
+ Printf(true, " attached to point at: " PT_AS_STR, COSTR(SK.GetEntity(c->ptA), p));
+ Vector dv = c->disp.offset;
+ Printf(false, " distance = %Fi%s", SS.MmToString(dv.Magnitude()).c_str());
+ Printf(false, " d(x, y, z) = " PT_AS_STR_NO_LINK, COSTR_NO_LINK(dv));
+ }
} else if(c->HasLabel()) {
if(c->reference) {
Printf(false, "%FtREFERENCE%E %s", desc.c_str());
diff --git a/src/entity.cpp b/src/entity.cpp
index a0f2f946..dd42cba6 100644
--- a/src/entity.cpp
+++ b/src/entity.cpp
@@ -26,6 +26,9 @@ bool EntityBase::HasVector() const {
}
ExprVector EntityBase::VectorGetExprsInWorkplane(hEntity wrkpl) const {
+ if(IsFace()) {
+ return FaceGetNormalExprs();
+ }
switch(type) {
case Type::LINE_SEGMENT:
return (SK.GetEntity(point[0])->PointGetExprsInWorkplane(wrkpl)).Minus(
@@ -62,6 +65,9 @@ ExprVector EntityBase::VectorGetExprs() const {
}
Vector EntityBase::VectorGetNum() const {
+ if(IsFace()) {
+ return FaceGetNormalNum();
+ }
switch(type) {
case Type::LINE_SEGMENT:
return (SK.GetEntity(point[0])->PointGetNum()).Minus(
@@ -79,6 +85,9 @@ Vector EntityBase::VectorGetNum() const {
}
Vector EntityBase::VectorGetRefPoint() const {
+ if(IsFace()) {
+ return FaceGetPointNum();
+ }
switch(type) {
case Type::LINE_SEGMENT:
return ((SK.GetEntity(point[0])->PointGetNum()).Plus(
diff --git a/src/file.cpp b/src/file.cpp
index 0b5806c0..c68ba137 100644
--- a/src/file.cpp
+++ b/src/file.cpp
@@ -918,7 +918,7 @@ try_again:
switch(LocateImportedFile(linkFileRelative, canCancel)) {
case Platform::MessageDialog::Response::YES: {
Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
- dialog->AddFilters(Platform::SolveSpaceModelFileFilters);
+ dialog->AddFilters(Platform::SolveSpaceLinkFileFilters);
dialog->ThawChoices(settings, "LinkSketch");
dialog->SuggestFilename(linkFileRelative);
if(dialog->RunModal()) {
diff --git a/src/graphicswin.cpp b/src/graphicswin.cpp
index c7c74d98..4236acf1 100644
--- a/src/graphicswin.cpp
+++ b/src/graphicswin.cpp
@@ -433,6 +433,11 @@ void GraphicsWindow::Init() {
// a canvas.
window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4);
window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT);
+ window->onContextLost = [&] {
+ canvas = NULL;
+ persistentCanvas = NULL;
+ persistentDirty = true;
+ };
window->onRender = std::bind(&GraphicsWindow::Paint, this);
window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1);
window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1);
@@ -712,16 +717,47 @@ double GraphicsWindow::ZoomToFit(const Camera &camera,
return scale;
}
+
+void GraphicsWindow::ZoomToMouse(double zoomMultiplyer) {
+ double offsetRight = offset.Dot(projRight);
+ double offsetUp = offset.Dot(projUp);
+
+ double width, height;
+ window->GetContentSize(&width, &height);
+
+ double righti = currentMousePosition.x / scale - offsetRight;
+ double upi = currentMousePosition.y / scale - offsetUp;
+
+ // zoomMultiplyer of 1 gives a default zoom factor of 1.2x: zoomMultiplyer * 1.2
+ // zoom = adjusted zoom negative zoomMultiplyer will zoom out, positive will zoom in
+ //
+
+ scale *= exp(0.1823216 * zoomMultiplyer); // ln(1.2) = 0.1823216
+
+ double rightf = currentMousePosition.x / scale - offsetRight;
+ double upf = currentMousePosition.y / scale - offsetUp;
+
+ offset = offset.Plus(projRight.ScaledBy(rightf - righti));
+ offset = offset.Plus(projUp.ScaledBy(upf - upi));
+
+ if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
+ if(havePainted) {
+ SS.ScheduleShowTW();
+ }
+ }
+ havePainted = false;
+ Invalidate();
+}
+
+
void GraphicsWindow::MenuView(Command id) {
switch(id) {
case Command::ZOOM_IN:
- SS.GW.scale *= 1.2;
- SS.ScheduleShowTW();
+ SS.GW.ZoomToMouse(1);
break;
case Command::ZOOM_OUT:
- SS.GW.scale /= 1.2;
- SS.ScheduleShowTW();
+ SS.GW.ZoomToMouse(-1);
break;
case Command::ZOOM_TO_FIT:
@@ -781,13 +817,18 @@ void GraphicsWindow::MenuView(Command id) {
Quaternion quatf = quat0;
double dmin = 1e10;
- // There are 24 possible views; 3*2*2*2
- int i, j, negi, negj;
- for(i = 0; i < 3; i++) {
- for(j = 0; j < 3; j++) {
+ // There are 24 possible views (3*2*2*2), if all are
+ // allowed. If the user is in turn-table mode, the
+ // isometric view must have the z-axis facing up, leaving
+ // 8 possible views (2*1*2*2).
+
+ bool require_turntable = (id==Command::NEAREST_ISO && SS.turntableNav);
+ for(int i = 0; i < 3; i++) {
+ for(int j = 0; j < 3; j++) {
if(i == j) continue;
- for(negi = 0; negi < 2; negi++) {
- for(negj = 0; negj < 2; negj++) {
+ if(require_turntable && (j!=2)) continue;
+ for(int negi = 0; negi < 2; negi++) {
+ for(int negj = 0; negj < 2; negj++) {
Vector ou = ortho[i], ov = ortho[j];
if(negi) ou = ou.ScaledBy(-1);
if(negj) ov = ov.ScaledBy(-1);
diff --git a/src/mouse.cpp b/src/mouse.cpp
index 5e7cde26..d183f58a 100644
--- a/src/mouse.cpp
+++ b/src/mouse.cpp
@@ -914,7 +914,7 @@ bool GraphicsWindow::MouseEvent(Platform::MouseEvent event) {
break;
case MouseEvent::Type::SCROLL_VERT:
- this->MouseScroll(event.x, event.y, event.shiftDown ? event.scrollDelta / 10 : event.scrollDelta);
+ this->MouseScroll(event.shiftDown ? event.scrollDelta / 10 : event.scrollDelta);
break;
case MouseEvent::Type::LEAVE:
@@ -1478,17 +1478,10 @@ void GraphicsWindow::EditControlDone(const std::string &s) {
}
}
-void GraphicsWindow::MouseScroll(double x, double y, double delta) {
- double offsetRight = offset.Dot(projRight);
- double offsetUp = offset.Dot(projUp);
-
- double righti = x/scale - offsetRight;
- double upi = y/scale - offsetUp;
-
- // The default zoom factor is 1.2x for one scroll wheel click (delta==1).
+void GraphicsWindow::MouseScroll(double zoomMultiplyer) {
// To support smooth scrolling where scroll wheel events come in increments
// smaller (or larger) than 1 we do:
- // scale *= exp(ln(1.2) * delta);
+ // scale *= exp(ln(1.2) * zoomMultiplyer);
// to ensure that the same total scroll delta always results in the same
// total zoom irrespective of in how many increments the zoom was applied.
// For example if we scroll a total delta of a+b in two events vs. one then
@@ -1496,21 +1489,7 @@ void GraphicsWindow::MouseScroll(double x, double y, double delta) {
// while
// scale * a * b != scale * (a+b)
// So this constant is ln(1.2) = 0.1823216 to make the default zoom 1.2x
- scale *= exp(0.1823216 * delta);
-
- double rightf = x/scale - offsetRight;
- double upf = y/scale - offsetUp;
-
- offset = offset.Plus(projRight.ScaledBy(rightf - righti));
- offset = offset.Plus(projUp.ScaledBy(upf - upi));
-
- if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
- if(havePainted) {
- SS.ScheduleShowTW();
- }
- }
- havePainted = false;
- Invalidate();
+ ZoomToMouse(zoomMultiplyer);
}
void GraphicsWindow::MouseLeave() {
diff --git a/src/platform/gui.h b/src/platform/gui.h
index f63fa80b..4f3d1316 100644
--- a/src/platform/gui.h
+++ b/src/platform/gui.h
@@ -221,6 +221,7 @@ public:
std::function onKeyboardEvent;
std::function onEditingDone;
std::function onScrollbarAdjusted;
+ std::function onContextLost;
std::function onRender;
virtual ~Window() = default;
@@ -229,7 +230,7 @@ public:
virtual double GetPixelDensity() = 0;
// Returns raster graphics and coordinate scale (already applied on the platform side),
// i.e. size of logical pixel in physical pixels, or device pixel ratio.
- virtual int GetDevicePixelRatio() = 0;
+ virtual double GetDevicePixelRatio() = 0;
// Returns (fractional) font scale, to be applied on top of (integral) device pixel ratio.
virtual double GetDeviceFontScale() {
return GetPixelDensity() / GetDevicePixelRatio() / 96.0;
diff --git a/src/platform/guigtk.cpp b/src/platform/guigtk.cpp
index 1a11582e..cd1e93fc 100644
--- a/src/platform/guigtk.cpp
+++ b/src/platform/guigtk.cpp
@@ -889,7 +889,7 @@ public:
return gtkWindow.get_screen()->get_resolution();
}
- int GetDevicePixelRatio() override {
+ double GetDevicePixelRatio() override {
return gtkWindow.get_scale_factor();
}
diff --git a/src/platform/guihtml.cpp b/src/platform/guihtml.cpp
new file mode 100644
index 00000000..7e49cc98
--- /dev/null
+++ b/src/platform/guihtml.cpp
@@ -0,0 +1,1464 @@
+//-----------------------------------------------------------------------------
+// The Emscripten-based implementation of platform-dependent GUI functionality.
+//
+// Copyright 2018 whitequark
+//-----------------------------------------------------------------------------
+#include
+#include
+#include
+#include "config.h"
+#include "solvespace.h"
+
+using namespace emscripten;
+
+namespace SolveSpace {
+namespace Platform {
+
+//-----------------------------------------------------------------------------
+// Emscripten API bridging
+//-----------------------------------------------------------------------------
+
+#define sscheck(expr) do { \
+ EMSCRIPTEN_RESULT emResult = (EMSCRIPTEN_RESULT)(expr); \
+ if(emResult < 0) \
+ HandleError(__FILE__, __LINE__, __func__, #expr, emResult); \
+ } while(0)
+
+static void HandleError(const char *file, int line, const char *function, const char *expr,
+ EMSCRIPTEN_RESULT emResult) {
+ const char *error = "Unknown error";
+ switch(emResult) {
+ case EMSCRIPTEN_RESULT_DEFERRED: error = "Deferred"; break;
+ case EMSCRIPTEN_RESULT_NOT_SUPPORTED: error = "Not supported"; break;
+ case EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED: error = "Failed (not deferred)"; break;
+ case EMSCRIPTEN_RESULT_INVALID_TARGET: error = "Invalid target"; break;
+ case EMSCRIPTEN_RESULT_UNKNOWN_TARGET: error = "Unknown target"; break;
+ case EMSCRIPTEN_RESULT_INVALID_PARAM: error = "Invalid parameter"; break;
+ case EMSCRIPTEN_RESULT_FAILED: error = "Failed"; break;
+ case EMSCRIPTEN_RESULT_NO_DATA: error = "No data"; break;
+ }
+
+ std::string message;
+ message += ssprintf("File %s, line %u, function %s:\n", file, line, function);
+ message += ssprintf("Emscripten API call failed: %s.\n", expr);
+ message += ssprintf("Error: %s\n", error);
+ FatalError(message);
+}
+
+static val Wrap(const std::string& str) {
+ // FIXME(emscripten): a nicer way to do this?
+ EM_ASM($Wrap$ret = UTF8ToString($0), str.c_str());
+ return val::global("window")["$Wrap$ret"];
+}
+
+static std::string Unwrap(val emStr) {
+ // FIXME(emscripten): a nicer way to do this?
+ val emArray = val::global("window").call("intArrayFromString", emStr, true) ;
+ val::global("window").set("$Wrap$input", emArray);
+ char *strC = (char *)EM_ASM_INT(return allocate($Wrap$input, ALLOC_NORMAL));
+ std::string str(strC, emArray["length"].as());
+ free(strC);
+ return str;
+}
+
+static void CallStdFunction(void *data) {
+ std::function *func = (std::function *)data;
+ if(*func) {
+ (*func)();
+ }
+}
+
+static val Wrap(std::function *func) {
+ EM_ASM($Wrap$ret = Module.dynCall_vi.bind(null, $0, $1), CallStdFunction, func);
+ return val::global("window")["$Wrap$ret"];
+}
+
+//-----------------------------------------------------------------------------
+// Fatal errors
+//-----------------------------------------------------------------------------
+
+void FatalError(const std::string &message) {
+ dbp("%s", message.c_str());
+#ifndef NDEBUG
+ emscripten_debugger();
+#endif
+ abort();
+}
+
+//-----------------------------------------------------------------------------
+// Settings
+//-----------------------------------------------------------------------------
+
+class SettingsImplHtml : public Settings {
+public:
+ void FreezeInt(const std::string &key, uint32_t value) {
+ val::global("localStorage").call("setItem", Wrap(key), value);
+ }
+
+ uint32_t ThawInt(const std::string &key, uint32_t defaultValue = 0) {
+ val value = val::global("localStorage").call("getItem", Wrap(key));
+ if(value == val::null())
+ return defaultValue;
+ return val::global("parseInt")(value, 0).as();
+ }
+
+ void FreezeFloat(const std::string &key, double value) {
+ val::global("localStorage").call("setItem", Wrap(key), value);
+ }
+
+ double ThawFloat(const std::string &key, double defaultValue = 0.0) {
+ val value = val::global("localStorage").call("getItem", Wrap(key));
+ if(value == val::null())
+ return defaultValue;
+ return val::global("parseFloat")(value).as();
+ }
+
+ void FreezeString(const std::string &key, const std::string &value) {
+ val::global("localStorage").call("setItem", Wrap(key), value);
+ }
+
+ std::string ThawString(const std::string &key,
+ const std::string &defaultValue = "") {
+ val value = val::global("localStorage").call("getItem", Wrap(key));
+ if(value == val::null())
+ return defaultValue;
+ return Unwrap(value);
+ }
+};
+
+SettingsRef GetSettings() {
+ return std::make_shared();
+}
+
+//-----------------------------------------------------------------------------
+// Timers
+//-----------------------------------------------------------------------------
+
+class TimerImplHtml : public Timer {
+public:
+ static void Callback(void *arg) {
+ TimerImplHtml *timer = (TimerImplHtml *)arg;
+ if(timer->onTimeout) {
+ timer->onTimeout();
+ }
+ }
+
+ void RunAfter(unsigned milliseconds) override {
+ emscripten_async_call(TimerImplHtml::Callback, this, milliseconds + 1);
+ }
+
+ void RunAfterNextFrame() override {
+ emscripten_async_call(TimerImplHtml::Callback, this, 0);
+ }
+
+ void RunAfterProcessingEvents() override {
+ emscripten_push_uncounted_main_loop_blocker(TimerImplHtml::Callback, this);
+ }
+};
+
+TimerRef CreateTimer() {
+ return std::unique_ptr(new TimerImplHtml);
+}
+
+//-----------------------------------------------------------------------------
+// Menus
+//-----------------------------------------------------------------------------
+
+class MenuItemImplHtml : public MenuItem {
+public:
+ val htmlMenuItem;
+
+ MenuItemImplHtml() :
+ htmlMenuItem(val::global("document").call("createElement", val("li")))
+ {}
+
+ void SetAccelerator(KeyboardEvent accel) override {
+ val htmlAccel = htmlMenuItem.call("querySelector", val(".accel"));
+ if(htmlAccel.as()) {
+ htmlAccel.call("remove");
+ }
+ htmlAccel = val::global("document").call("createElement", val("span"));
+ htmlAccel.call("setAttribute", val("class"), val("accel"));
+ htmlAccel.set("innerText", AcceleratorDescription(accel));
+ htmlMenuItem.call("appendChild", htmlAccel);
+ }
+
+ void SetIndicator(Indicator type) override {
+ val htmlClasses = htmlMenuItem["classList"];
+ htmlClasses.call("remove", val("check"));
+ htmlClasses.call("remove", val("radio"));
+ switch(type) {
+ case Indicator::NONE:
+ break;
+
+ case Indicator::CHECK_MARK:
+ htmlClasses.call("add", val("check"));
+ break;
+
+ case Indicator::RADIO_MARK:
+ htmlClasses.call("add", val("radio"));
+ break;
+ }
+ }
+
+ void SetEnabled(bool enabled) override {
+ if(enabled) {
+ htmlMenuItem["classList"].call("remove", val("disabled"));
+ } else {
+ htmlMenuItem["classList"].call("add", val("disabled"));
+ }
+ }
+
+ void SetActive(bool active) override {
+ if(active) {
+ htmlMenuItem["classList"].call("add", val("active"));
+ } else {
+ htmlMenuItem["classList"].call("remove", val("active"));
+ }
+ }
+};
+
+class MenuImplHtml;
+
+static std::shared_ptr popupMenuOnScreen;
+
+class MenuImplHtml : public Menu,
+ public std::enable_shared_from_this {
+public:
+ val htmlMenu;
+
+ std::vector> menuItems;
+ std::vector> subMenus;
+
+ std::function popupDismissFunc;
+
+ MenuImplHtml() :
+ htmlMenu(val::global("document").call("createElement", val("ul")))
+ {
+ htmlMenu["classList"].call("add", val("menu"));
+ }
+
+ MenuItemRef AddItem(const std::string &label, std::function onTrigger,
+ bool mnemonics = true) override {
+ std::shared_ptr menuItem = std::make_shared();
+ menuItems.push_back(menuItem);
+ menuItem->onTrigger = onTrigger;
+
+ if(mnemonics) {
+ val::global("window").call("setLabelWithMnemonic", menuItem->htmlMenuItem,
+ Wrap(label));
+ } else {
+ val htmlLabel = val::global("document").call("createElement", val("span"));
+ htmlLabel["classList"].call("add", val("label"));
+ htmlLabel.set("innerText", Wrap(label));
+ menuItem->htmlMenuItem.call("appendChild", htmlLabel);
+ }
+ menuItem->htmlMenuItem.call("addEventListener", val("trigger"),
+ Wrap(&menuItem->onTrigger));
+ htmlMenu.call("appendChild", menuItem->htmlMenuItem);
+ return menuItem;
+ }
+
+ std::shared_ptr AddSubMenu(const std::string &label) override {
+ val htmlMenuItem = val::global("document").call("createElement", val("li"));
+ val::global("window").call("setLabelWithMnemonic", htmlMenuItem, Wrap(label));
+ htmlMenuItem["classList"].call("add", val("has-submenu"));
+ htmlMenu.call("appendChild", htmlMenuItem);
+
+ std::shared_ptr subMenu = std::make_shared();
+ subMenus.push_back(subMenu);
+ htmlMenuItem.call("appendChild", subMenu->htmlMenu);
+ return subMenu;
+ }
+
+ void AddSeparator() override {
+ val htmlSeparator = val::global("document").call("createElement", val("li"));
+ htmlSeparator["classList"].call("add", val("separator"));
+ htmlMenu.call("appendChild", htmlSeparator);
+ }
+
+ void PopUp() override {
+ if(popupMenuOnScreen) {
+ popupMenuOnScreen->htmlMenu.call("remove");
+ popupMenuOnScreen = NULL;
+ }
+
+ EmscriptenMouseEvent emStatus = {};
+ sscheck(emscripten_get_mouse_status(&emStatus));
+ htmlMenu["classList"].call("add", val("popup"));
+ htmlMenu["style"].set("left", std::to_string(emStatus.clientX) + "px");
+ htmlMenu["style"].set("top", std::to_string(emStatus.clientY) + "px");
+
+ val::global("document")["body"].call("appendChild", htmlMenu);
+ popupMenuOnScreen = shared_from_this();
+ }
+
+ void Clear() override {
+ while(htmlMenu["childElementCount"].as() > 0) {
+ htmlMenu["firstChild"].call("remove");
+ }
+ }
+};
+
+MenuRef CreateMenu() {
+ return std::make_shared();
+}
+
+class MenuBarImplHtml final : public MenuBar {
+public:
+ val htmlMenuBar;
+
+ std::vector> subMenus;
+
+ MenuBarImplHtml() :
+ htmlMenuBar(val::global("document").call("createElement", val("ul")))
+ {
+ htmlMenuBar["classList"].call("add", val("menu"));
+ htmlMenuBar["classList"].call("add", val("menubar"));
+ }
+
+ std::shared_ptr AddSubMenu(const std::string &label) override {
+ val htmlMenuItem = val::global("document").call("createElement", val("li"));
+ val::global("window").call("setLabelWithMnemonic", htmlMenuItem, Wrap(label));
+ htmlMenuBar.call("appendChild", htmlMenuItem);
+
+ std::shared_ptr subMenu = std::make_shared();
+ subMenus.push_back(subMenu);
+ htmlMenuItem.call("appendChild", subMenu->htmlMenu);
+ return subMenu;
+ }
+
+ void Clear() override {
+ while(htmlMenuBar["childElementCount"].as() > 0) {
+ htmlMenuBar["firstChild"].call("remove");
+ }
+ }
+};
+
+MenuBarRef GetOrCreateMainMenu(bool *unique) {
+ *unique = false;
+ return std::make_shared();
+}
+
+//-----------------------------------------------------------------------------
+// Windows
+//-----------------------------------------------------------------------------
+
+class TouchEventHelper {
+public:
+ // FIXME(emscripten): Workaround. touchstart and touchend repeats two times.
+ bool touchActionStarted = false;
+
+ int previousNumTouches = 0;
+
+ double centerX = 0;
+ double centerY = 0;
+
+ // double startPinchDistance = 0;
+ double previousPinchDistance = 0;
+
+ std::function onPointerDown;
+ std::function onPointerMove;
+ std::function onPointerUp;
+ std::function onScroll;
+
+ void clear(void) {
+ touchActionStarted = false;
+ previousNumTouches = 0;
+ centerX = 0;
+ centerY = 0;
+ // startPinchDistance = 0;
+ previousPinchDistance = 0;
+ }
+
+ void calculateCenterPosition(const EmscriptenTouchEvent& emEvent, double& dst_x, double& dst_y) {
+ double x = 0;
+ double y = 0;
+ for (int i = 0; i < emEvent.numTouches; i++) {
+ x += emEvent.touches[i].targetX;
+ y += emEvent.touches[i].targetY;
+ }
+ dst_x = x / emEvent.numTouches;
+ dst_y = y / emEvent.numTouches;
+ }
+
+ void calculatePinchDistance(const EmscriptenTouchEvent& emEvent, double& dst_distance) {
+ if (emEvent.numTouches < 2) {
+ return;
+ }
+ double x1 = emEvent.touches[0].targetX;
+ double y1 = emEvent.touches[0].targetY;
+ double x2 = emEvent.touches[1].targetX;
+ double y2 = emEvent.touches[1].targetY;
+ dst_distance = std::sqrt(std::pow(x1 - x2, 2) + std::pow(y1 - y2, 2));
+ }
+
+ void createMouseEventPRESS(const EmscriptenTouchEvent& emEvent, MouseEvent& dst_mouseevent) {
+ double x = 0, y = 0;
+ this->calculateCenterPosition(emEvent, x, y);
+ this->centerX = x;
+ this->centerY = y;
+ this->touchActionStarted = true;
+ this->previousNumTouches = emEvent.numTouches;
+ dst_mouseevent.type = MouseEvent::Type::PRESS;
+ dst_mouseevent.x = x;
+ dst_mouseevent.y = y;
+ dst_mouseevent.shiftDown = emEvent.shiftKey;
+ dst_mouseevent.controlDown = emEvent.ctrlKey;
+ switch(emEvent.numTouches) {
+ case 1:
+ dst_mouseevent.button = MouseEvent::Button::LEFT;
+ break;
+ case 2: {
+ dst_mouseevent.button = MouseEvent::Button::RIGHT;
+ // double distance = 0;
+ this->calculatePinchDistance(emEvent, this->previousPinchDistance);
+ // this->startPinchDistance = distance;
+ // this->previousPinchDistance = distance;
+ break;
+ }
+ default:
+ dst_mouseevent.button = MouseEvent::Button::MIDDLE;
+ break;
+ }
+ }
+
+ void createMouseEventRELEASE(const EmscriptenTouchEvent& emEvent, MouseEvent& dst_mouseevent) {
+ this->calculateCenterPosition(emEvent, this->centerX, this->centerY);
+ this->previousNumTouches = 0;
+ dst_mouseevent.type = MouseEvent::Type::RELEASE;
+ dst_mouseevent.x = this->centerX;
+ dst_mouseevent.y = this->centerY;
+ dst_mouseevent.shiftDown = emEvent.shiftKey;
+ dst_mouseevent.controlDown = emEvent.ctrlKey;
+ switch(this->previousNumTouches) {
+ case 1:
+ dst_mouseevent.button = MouseEvent::Button::LEFT;
+ break;
+ case 2:
+ dst_mouseevent.button = MouseEvent::Button::RIGHT;
+ break;
+ default:
+ dst_mouseevent.button = MouseEvent::Button::MIDDLE;
+ break;
+ }
+ }
+
+ void createMouseEventMOTION(const EmscriptenTouchEvent& emEvent, MouseEvent& dst_mouseevent) {
+ dst_mouseevent.type = MouseEvent::Type::MOTION;
+ this->calculateCenterPosition(emEvent, this->centerX, this->centerY);
+ dst_mouseevent.x = this->centerX;
+ dst_mouseevent.y = this->centerY;
+ dst_mouseevent.shiftDown = emEvent.shiftKey;
+ dst_mouseevent.controlDown = emEvent.ctrlKey;
+ switch(emEvent.numTouches) {
+ case 1:
+ dst_mouseevent.button = MouseEvent::Button::LEFT;
+ break;
+ case 2:
+ dst_mouseevent.button = MouseEvent::Button::RIGHT;
+ break;
+ default:
+ dst_mouseevent.button = MouseEvent::Button::MIDDLE;
+ break;
+ }
+ }
+
+ void createMouseEventSCROLL(const EmscriptenTouchEvent& emEvent, MouseEvent& event) {
+ event.type = MouseEvent::Type::SCROLL_VERT;
+ double newDistance = 0;
+ this->calculatePinchDistance(emEvent, newDistance);
+ this->calculateCenterPosition(emEvent, this->centerX, this->centerY);
+ event.x = this->centerX;
+ event.y = this->centerY;
+ event.shiftDown = emEvent.shiftKey;
+ event.controlDown = emEvent.ctrlKey;
+ // FIXME(emscripten): best value range for scrollDelta ?
+ event.scrollDelta = (newDistance - this->previousPinchDistance) / 25.0;
+ if (std::abs(event.scrollDelta) > 2) {
+ event.scrollDelta = 2;
+ if (std::signbit(event.scrollDelta)) {
+ event.scrollDelta *= -1.0;
+ }
+ }
+ this->previousPinchDistance = newDistance;
+ }
+
+ void onTouchStart(const EmscriptenTouchEvent& emEvent, void* callbackParameter) {
+ if (this->touchActionStarted) {
+ // dbp("onTouchStart(): Break due to already started.");
+ return;
+ }
+
+ MouseEvent event;
+ this->createMouseEventPRESS(emEvent, event);
+ this->previousNumTouches = emEvent.numTouches;
+ if (this->onPointerDown) {
+ // dbp("onPointerDown(): numTouches=%d, timestamp=%f", emEvent.numTouches, emEvent.timestamp);
+ this->onPointerDown(&event, callbackParameter);
+ }
+ }
+
+ void onTouchMove(const EmscriptenTouchEvent& emEvent, void* callbackParameter) {
+ this->calculateCenterPosition(emEvent, this->centerX, this->centerY);
+ int newNumTouches = emEvent.numTouches;
+ if (newNumTouches != this->previousNumTouches) {
+ MouseEvent releaseEvent;
+
+ this->createMouseEventRELEASE(emEvent, releaseEvent);
+ if (this->onPointerUp) {
+ // dbp("onPointerUp(): numTouches=%d, timestamp=%f", emEvent.numTouches, emEvent.timestamp);
+ this->onPointerUp(&releaseEvent, callbackParameter);
+ }
+
+ MouseEvent pressEvent;
+
+ this->createMouseEventPRESS(emEvent, pressEvent);
+ if (this->onPointerDown) {
+ // dbp("onPointerDown(): numTouches=%d, timestamp=%f", emEvent.numTouches, emEvent.timestamp);
+ this->onPointerDown(&pressEvent, callbackParameter);
+ }
+ }
+
+ MouseEvent motionEvent = { };
+ this->createMouseEventMOTION(emEvent, motionEvent);
+
+ if (this->onPointerMove) {
+ // dbp("onPointerMove(): numTouches=%d, timestamp=%f", emEvent.numTouches, emEvent.timestamp);
+ this->onPointerMove(&motionEvent, callbackParameter);
+ }
+
+ if (emEvent.numTouches == 2) {
+ MouseEvent scrollEvent;
+ this->createMouseEventSCROLL(emEvent, scrollEvent);
+ if (scrollEvent.scrollDelta != 0) {
+ if (this->onScroll) {
+ // dbp("Scroll %f", scrollEvent.scrollDelta);
+ this->onScroll(&scrollEvent, callbackParameter);
+ }
+ }
+ }
+
+ this->previousNumTouches = emEvent.numTouches;
+ }
+
+ void onTouchEnd(const EmscriptenTouchEvent& emEvent, void* callbackParameter) {
+ if (!this->touchActionStarted) {
+ return;
+ }
+
+ MouseEvent releaseEvent = { };
+ this->createMouseEventRELEASE(emEvent, releaseEvent);
+
+ if (this->onPointerUp) {
+ // dbp("onPointerUp(): numTouches=%d, timestamp=%d", emEvent.numTouches, emEvent.timestamp);
+ this->onPointerUp(&releaseEvent, callbackParameter);
+ }
+
+ this->clear();
+ }
+
+ void onTouchCancel(const EmscriptenTouchEvent& emEvent, void* callbackParameter) {
+ this->onTouchEnd(emEvent, callbackParameter);
+ }
+};
+
+static TouchEventHelper touchEventHelper;
+
+static KeyboardEvent handledKeyboardEvent;
+
+class WindowImplHtml final : public Window {
+public:
+ std::string emCanvasSel;
+ EMSCRIPTEN_WEBGL_CONTEXT_HANDLE emContext = 0;
+
+ val htmlContainer;
+ val htmlEditor;
+ val scrollbarHelper;
+
+ std::function editingDoneFunc;
+ std::shared_ptr menuBar;
+
+ WindowImplHtml(val htmlContainer, std::string emCanvasSel) :
+ emCanvasSel(emCanvasSel),
+ htmlContainer(htmlContainer),
+ htmlEditor(val::global("document").call("createElement", val("input")))
+ {
+ htmlEditor["classList"].call("add", val("editor"));
+ htmlEditor["style"].set("display", "none");
+ editingDoneFunc = [this] {
+ if(onEditingDone) {
+ onEditingDone(Unwrap(htmlEditor["value"]));
+ }
+ };
+ htmlEditor.call("addEventListener", val("trigger"), Wrap(&editingDoneFunc));
+ htmlContainer["parentElement"].call("appendChild", htmlEditor);
+
+ std::string scrollbarElementQuery = emCanvasSel + "scrollbar";
+ dbp("scrollbar element query: \"%s\"", scrollbarElementQuery.c_str());
+ val scrollbarElement = val::global("document").call("querySelector", val(scrollbarElementQuery));
+ if (scrollbarElement == val::null()) {
+ // dbp("scrollbar element is null.");
+ this->scrollbarHelper = val::null();
+ } else {
+ dbp("scrollbar element OK.");
+ this->scrollbarHelper = val::global("window")["ScrollbarHelper"].new_(val(scrollbarElementQuery));
+ static std::function onScrollCallback = [this] {
+ // dbp("onScrollCallback std::function this=%p", (void*)this);
+ if (this->onScrollbarAdjusted) {
+ double newpos = this->scrollbarHelper.call("getScrollbarPosition");
+ // dbp(" call onScrollbarAdjusted(%f)", newpos);
+ this->onScrollbarAdjusted(newpos);
+ }
+ this->Invalidate();
+ };
+ this->scrollbarHelper.set("onScrollCallback", Wrap(&onScrollCallback));
+ }
+
+ sscheck(emscripten_set_resize_callback(
+ EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false,
+ WindowImplHtml::ResizeCallback));
+ sscheck(emscripten_set_resize_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::ResizeCallback));
+ sscheck(emscripten_set_mousemove_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::MouseCallback));
+ sscheck(emscripten_set_mousedown_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::MouseCallback));
+ sscheck(emscripten_set_click_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::MouseCallback));
+ sscheck(emscripten_set_dblclick_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::MouseCallback));
+ sscheck(emscripten_set_mouseup_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::MouseCallback));
+ sscheck(emscripten_set_mouseleave_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::MouseCallback));
+
+ sscheck(emscripten_set_touchstart_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::TouchCallback));
+ sscheck(emscripten_set_touchmove_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::TouchCallback));
+ sscheck(emscripten_set_touchend_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::TouchCallback));
+ sscheck(emscripten_set_touchcancel_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::TouchCallback));
+
+ sscheck(emscripten_set_wheel_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::WheelCallback));
+ sscheck(emscripten_set_keydown_callback(
+ EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false,
+ WindowImplHtml::KeyboardCallback));
+ sscheck(emscripten_set_keyup_callback(
+ EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false,
+ WindowImplHtml::KeyboardCallback));
+ sscheck(emscripten_set_webglcontextlost_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::ContextLostCallback));
+ sscheck(emscripten_set_webglcontextrestored_callback(
+ emCanvasSel.c_str(), this, /*useCapture=*/false,
+ WindowImplHtml::ContextRestoredCallback));
+
+ ResizeCanvasElement();
+ SetupWebGLContext();
+ }
+
+ ~WindowImplHtml() {
+ if(emContext != 0) {
+ sscheck(emscripten_webgl_destroy_context(emContext));
+ }
+ }
+
+ static EM_BOOL ResizeCallback(int emEventType, const EmscriptenUiEvent *emEvent, void *data) {
+ WindowImplHtml *window = (WindowImplHtml *)data;
+ window->Invalidate();
+ return EM_TRUE;
+ }
+
+ static EM_BOOL MouseCallback(int emEventType, const EmscriptenMouseEvent *emEvent,
+ void *data) {
+ if(val::global("window").call("isModal")) return EM_FALSE;
+
+ WindowImplHtml *window = (WindowImplHtml *)data;
+ MouseEvent event = {};
+ switch(emEventType) {
+ case EMSCRIPTEN_EVENT_MOUSEMOVE:
+ event.type = MouseEvent::Type::MOTION;
+ break;
+ case EMSCRIPTEN_EVENT_MOUSEDOWN:
+ event.type = MouseEvent::Type::PRESS;
+ break;
+ case EMSCRIPTEN_EVENT_DBLCLICK:
+ event.type = MouseEvent::Type::DBL_PRESS;
+ break;
+ case EMSCRIPTEN_EVENT_MOUSEUP:
+ event.type = MouseEvent::Type::RELEASE;
+ break;
+ case EMSCRIPTEN_EVENT_MOUSELEAVE:
+ event.type = MouseEvent::Type::LEAVE;
+ break;
+ default:
+ return EM_FALSE;
+ }
+ switch(emEventType) {
+ case EMSCRIPTEN_EVENT_MOUSEMOVE:
+ if(emEvent->buttons & 1) {
+ event.button = MouseEvent::Button::LEFT;
+ } else if(emEvent->buttons & 2) {
+ event.button = MouseEvent::Button::RIGHT;
+ } else if(emEvent->buttons & 4) {
+ event.button = MouseEvent::Button::MIDDLE;
+ }
+ break;
+ case EMSCRIPTEN_EVENT_MOUSEDOWN:
+ case EMSCRIPTEN_EVENT_DBLCLICK:
+ case EMSCRIPTEN_EVENT_MOUSEUP:
+ switch(emEvent->button) {
+ case 0: event.button = MouseEvent::Button::LEFT; break;
+ case 1: event.button = MouseEvent::Button::MIDDLE; break;
+ case 2: event.button = MouseEvent::Button::RIGHT; break;
+ }
+ break;
+ default:
+ return EM_FALSE;
+ }
+ event.x = emEvent->targetX;
+ event.y = emEvent->targetY;
+ event.shiftDown = emEvent->shiftKey || emEvent->altKey;
+ event.controlDown = emEvent->ctrlKey;
+
+ if(window->onMouseEvent) {
+ return window->onMouseEvent(event);
+ }
+ return EM_FALSE;
+ }
+
+ static EM_BOOL TouchCallback(int emEventType, const EmscriptenTouchEvent *emEvent,
+ void *data) {
+
+ if(val::global("window").call("isModal")) return EM_FALSE;
+
+ static bool initialized = false;
+
+ WindowImplHtml *window = (WindowImplHtml *)data;
+
+ if (!initialized) {
+ touchEventHelper.onPointerDown = [](MouseEvent* event, void* param) -> void {
+ WindowImplHtml* window = (WindowImplHtml*)param;
+ if (window->onMouseEvent) {
+ window->onMouseEvent(*event);
+ }
+ };
+ touchEventHelper.onPointerMove = [](MouseEvent* event, void* param) -> void {
+ WindowImplHtml* window = (WindowImplHtml*)param;
+ if (window->onMouseEvent) {
+ window->onMouseEvent(*event);
+ }
+ };
+ touchEventHelper.onPointerUp = [](MouseEvent* event, void* param) -> void {
+ WindowImplHtml* window = (WindowImplHtml*)param;
+ if (window->onMouseEvent) {
+ window->onMouseEvent(*event);
+ }
+ };
+ touchEventHelper.onScroll = [](MouseEvent* event, void* param) -> void {
+ WindowImplHtml* window = (WindowImplHtml*)param;
+ if (window->onMouseEvent) {
+ window->onMouseEvent(*event);
+ }
+ };
+ initialized = true;
+ }
+
+ switch(emEventType) {
+ case EMSCRIPTEN_EVENT_TOUCHSTART:
+ touchEventHelper.onTouchStart(*emEvent, window);
+ break;
+ case EMSCRIPTEN_EVENT_TOUCHMOVE:
+ touchEventHelper.onTouchMove(*emEvent, window);
+ break;
+ case EMSCRIPTEN_EVENT_TOUCHEND:
+ touchEventHelper.onTouchEnd(*emEvent, window);
+ break;
+ case EMSCRIPTEN_EVENT_TOUCHCANCEL:
+ touchEventHelper.onTouchCancel(*emEvent, window);
+ break;
+ default:
+ return EM_FALSE;
+ }
+
+ return true;
+ }
+
+ static EM_BOOL WheelCallback(int emEventType, const EmscriptenWheelEvent *emEvent,
+ void *data) {
+ if(val::global("window").call("isModal")) return EM_FALSE;
+
+ WindowImplHtml *window = (WindowImplHtml *)data;
+ MouseEvent event = {};
+ if(emEvent->deltaY != 0) {
+ event.type = MouseEvent::Type::SCROLL_VERT;
+ // FIXME(emscripten):
+ // Pay attention to:
+ // dbp("Mouse wheel delta mode: %lu", emEvent->deltaMode);
+ // https://emscripten.org/docs/api_reference/html5.h.html#id11
+ // https://www.w3.org/TR/DOM-Level-3-Events/#dom-wheelevent-deltamode
+ // and adjust the 0.01 below. deltaMode == 0 on a Firefox on a Windows.
+ event.scrollDelta = -emEvent->deltaY * 0.01;
+ } else {
+ return EM_FALSE;
+ }
+
+ const EmscriptenMouseEvent &emStatus = emEvent->mouse;
+ event.x = emStatus.targetX;
+ event.y = emStatus.targetY;
+ event.shiftDown = emStatus.shiftKey;
+ event.controlDown = emStatus.ctrlKey;
+
+ if(window->onMouseEvent) {
+ return window->onMouseEvent(event);
+ }
+ return EM_FALSE;
+ }
+
+ static EM_BOOL KeyboardCallback(int emEventType, const EmscriptenKeyboardEvent *emEvent,
+ void *data) {
+ if(emEvent->altKey) return EM_FALSE;
+ if(emEvent->repeat) return EM_FALSE;
+
+ WindowImplHtml *window = (WindowImplHtml *)data;
+ KeyboardEvent event = {};
+ switch(emEventType) {
+ case EMSCRIPTEN_EVENT_KEYDOWN:
+ event.type = KeyboardEvent::Type::PRESS;
+ break;
+
+ case EMSCRIPTEN_EVENT_KEYUP:
+ event.type = KeyboardEvent::Type::RELEASE;
+ break;
+
+ default:
+ return EM_FALSE;
+ }
+ event.shiftDown = emEvent->shiftKey;
+ event.controlDown = emEvent->ctrlKey;
+
+ std::string key = emEvent->key;
+ if(key[0] == 'F' && isdigit(key[1])) {
+ event.key = KeyboardEvent::Key::FUNCTION;
+ event.num = std::stol(key.substr(1));
+ } else {
+ event.key = KeyboardEvent::Key::CHARACTER;
+
+ auto utf8 = ReadUTF8(key);
+ if(++utf8.begin() == utf8.end()) {
+ event.chr = tolower(*utf8.begin());
+ } else if(key == "Escape") {
+ event.chr = '\e';
+ } else if(key == "Tab") {
+ event.chr = '\t';
+ } else if(key == "Backspace") {
+ event.chr = '\b';
+ } else if(key == "Delete") {
+ event.chr = '\x7f';
+ } else {
+ return EM_FALSE;
+ }
+
+ if(event.chr == '>' && event.shiftDown) {
+ event.shiftDown = false;
+ }
+ }
+
+ if(event.Equals(handledKeyboardEvent)) return EM_FALSE;
+ if(val::global("window").call("isModal")) {
+ handledKeyboardEvent = {};
+ return EM_FALSE;
+ }
+
+ if(window->onKeyboardEvent) {
+ if(window->onKeyboardEvent(event)) {
+ handledKeyboardEvent = event;
+ return EM_TRUE;
+ }
+ }
+ return EM_FALSE;
+ }
+
+ void SetupWebGLContext() {
+ EmscriptenWebGLContextAttributes emAttribs = {};
+ emscripten_webgl_init_context_attributes(&emAttribs);
+ emAttribs.alpha = false;
+ emAttribs.failIfMajorPerformanceCaveat = true;
+
+ sscheck(emContext = emscripten_webgl_create_context(emCanvasSel.c_str(), &emAttribs));
+ dbp("Canvas %s: got context %d", emCanvasSel.c_str(), emContext);
+ }
+
+ static int ContextLostCallback(int eventType, const void *reserved, void *data) {
+ WindowImplHtml *window = (WindowImplHtml *)data;
+ dbp("Canvas %s: context lost", window->emCanvasSel.c_str());
+ window->emContext = 0;
+
+ if(window->onContextLost) {
+ window->onContextLost();
+ }
+ return EM_TRUE;
+ }
+
+ static int ContextRestoredCallback(int eventType, const void *reserved, void *data) {
+ WindowImplHtml *window = (WindowImplHtml *)data;
+ dbp("Canvas %s: context restored", window->emCanvasSel.c_str());
+ window->SetupWebGLContext();
+ return EM_TRUE;
+ }
+
+ void ResizeCanvasElement() {
+ double width, height;
+ std::string htmlContainerSel = "#" + htmlContainer["id"].as();
+ sscheck(emscripten_get_element_css_size(htmlContainerSel.c_str(), &width, &height));
+ // sscheck(emscripten_get_element_css_size(emCanvasSel.c_str(), &width, &height));
+
+ double devicePixelRatio = GetDevicePixelRatio();
+ width *= devicePixelRatio;
+ height *= devicePixelRatio;
+
+ int currentWidth = 0, currentHeight = 0;
+ sscheck(emscripten_get_canvas_element_size(emCanvasSel.c_str(), ¤tWidth, ¤tHeight));
+
+ if ((int)width != currentWidth || (int)height != currentHeight) {
+ // dbp("Canvas %s container current size: (%d, %d)", emCanvasSel.c_str(), (int)currentWidth, (int)currentHeight);
+ // dbp("Canvas %s: resizing to (%d, %d)", emCanvasSel.c_str(), (int)width, (int)height);
+ sscheck(emscripten_set_canvas_element_size(emCanvasSel.c_str(), (int)width, (int)height));
+ }
+ }
+
+ static void RenderCallback(void *data) {
+ WindowImplHtml *window = (WindowImplHtml *)data;
+ if(window->emContext == 0) {
+ dbp("Canvas %s: cannot render: no context", window->emCanvasSel.c_str());
+ return;
+ }
+
+ window->ResizeCanvasElement();
+ sscheck(emscripten_webgl_make_context_current(window->emContext));
+ if(window->onRender) {
+ window->onRender();
+ }
+ }
+
+ double GetPixelDensity() override {
+ return 96.0 * GetDevicePixelRatio();
+ }
+
+ double GetDevicePixelRatio() override {
+ return emscripten_get_device_pixel_ratio();
+ }
+
+ bool IsVisible() override {
+ // FIXME(emscripten): implement
+ return true;
+ }
+
+ void SetVisible(bool visible) override {
+ // FIXME(emscripten): implement
+ }
+
+ void Focus() override {
+ // Do nothing, we can't affect focus of browser windows.
+ }
+
+ bool IsFullScreen() override {
+ EmscriptenFullscreenChangeEvent emEvent = {};
+ sscheck(emscripten_get_fullscreen_status(&emEvent));
+ return emEvent.isFullscreen;
+ }
+
+ void SetFullScreen(bool fullScreen) override {
+ if(fullScreen) {
+ EmscriptenFullscreenStrategy emStrategy = {};
+ emStrategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH;
+ emStrategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_HIDEF;
+ emStrategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
+ sscheck(emscripten_request_fullscreen_strategy(
+ emCanvasSel.c_str(), /*deferUntilInEventHandler=*/true, &emStrategy));
+ } else {
+ sscheck(emscripten_exit_fullscreen());
+ }
+ }
+
+ void SetTitle(const std::string &title) override {
+ // FIXME(emscripten): implement
+ }
+
+ void SetMenuBar(MenuBarRef menuBar) override {
+ std::shared_ptr menuBarImpl =
+ std::static_pointer_cast(menuBar);
+ this->menuBar = menuBarImpl;
+
+ val htmlMain = val::global("document").call("querySelector", val("main"));
+ val htmlCurrentMenuBar = htmlMain.call("querySelector", val(".menubar"));
+ if(htmlCurrentMenuBar.as()) {
+ htmlCurrentMenuBar.call("remove");
+ }
+ htmlMain.call("insertBefore", menuBarImpl->htmlMenuBar,
+ htmlMain["firstChild"]);
+ ResizeCanvasElement();
+ }
+
+ void GetContentSize(double *width, double *height) override {
+ sscheck(emscripten_get_element_css_size(emCanvasSel.c_str(), width, height));
+ }
+
+ void SetMinContentSize(double width, double height) override {
+ // Do nothing, we can't affect sizing of browser windows.
+ }
+
+ void FreezePosition(SettingsRef settings, const std::string &key) override {
+ // Do nothing, we can't position browser windows.
+ }
+
+ void ThawPosition(SettingsRef settings, const std::string &key) override {
+ // Do nothing, we can't position browser windows.
+ }
+
+ void SetCursor(Cursor cursor) override {
+ std::string htmlCursor;
+ switch(cursor) {
+ case Cursor::POINTER: htmlCursor = "default"; break;
+ case Cursor::HAND: htmlCursor = "pointer"; break;
+ }
+ htmlContainer["style"].set("cursor", htmlCursor);
+ }
+
+ void SetTooltip(const std::string &text, double x, double y,
+ double width, double height) override {
+ val htmlCanvas =
+ val::global("document").call("querySelector", emCanvasSel);
+ htmlCanvas.set("title", Wrap(text));
+ }
+
+ bool IsEditorVisible() override {
+ return htmlEditor["style"]["display"].as() != "none";
+ }
+
+ void ShowEditor(double x, double y, double fontHeight, double minWidth,
+ bool isMonospace, const std::string &text) override {
+ htmlEditor["style"].set("display", val(""));
+ val canvasClientRect = val::global("document").call("querySelector", val(this->emCanvasSel)).call("getBoundingClientRect");
+ double canvasLeft = canvasClientRect["left"].as();
+ double canvasTop = canvasClientRect["top"].as();
+ htmlEditor["style"].set("left", std::to_string(canvasLeft + x - 4) + "px");
+ htmlEditor["style"].set("top", std::to_string(canvasTop + y - fontHeight - 2) + "px");
+ htmlEditor["style"].set("fontSize", std::to_string(fontHeight) + "px");
+ htmlEditor["style"].set("minWidth", std::to_string(minWidth) + "px");
+ htmlEditor["style"].set("fontFamily", isMonospace ? "monospace" : "sans");
+ htmlEditor.set("value", Wrap(text));
+ htmlEditor.call("focus");
+ }
+
+ void HideEditor() override {
+ htmlEditor["style"].set("display", val("none"));
+ }
+
+ void SetScrollbarVisible(bool visible) override {
+ // dbp("SetScrollbarVisible(): visible=%d", visible ? 1 : 0);
+ if (this->scrollbarHelper == val::null()) {
+ // dbp("scrollbarHelper is null.");
+ return;
+ }
+ if (!visible) {
+ this->scrollbarHelper.call("setScrollbarEnabled", val(false));
+ }
+ }
+
+ double scrollbarPos = 0.0;
+ double scrollbarMin = 0.0;
+ double scrollbarMax = 0.0;
+ double scrollbarPageSize = 0.0;
+
+ void ConfigureScrollbar(double min, double max, double pageSize) override {
+ // dbp("ConfigureScrollbar(): min=%f, max=%f, pageSize=%f", min, max, pageSize);
+ if (this->scrollbarHelper == val::null()) {
+ // dbp("scrollbarHelper is null.");
+ return;
+ }
+ // FIXME(emscripten): implement
+ this->scrollbarMin = min;
+ this->scrollbarMax = max;
+ this->scrollbarPageSize = pageSize;
+
+ this->scrollbarHelper.call("setRange", this->scrollbarMin, this->scrollbarMax);
+ this->scrollbarHelper.call("setPageSize", pageSize);
+ }
+
+ double GetScrollbarPosition() override {
+ // dbp("GetScrollbarPosition()");
+ if (this->scrollbarHelper == val::null()) {
+ // dbp("scrollbarHelper is null.");
+ return 0;
+ }
+ this->scrollbarPos = this->scrollbarHelper.call("getScrollbarPosition");
+ // dbp(" GetScrollbarPosition() returns %f", this->scrollbarPos);
+ return scrollbarPos;
+ }
+
+ void SetScrollbarPosition(double pos) override {
+ // dbp("SetScrollbarPosition(): pos=%f", pos);
+ if (this->scrollbarHelper == val::null()) {
+ // dbp("scrollbarHelper is null.");
+ return;
+ }
+ this->scrollbarHelper.call("setScrollbarPosition", pos);
+ scrollbarPos = pos;
+ }
+
+ void Invalidate() override {
+ emscripten_async_call(WindowImplHtml::RenderCallback, this, -1);
+ }
+};
+
+WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) {
+ static int windowNum;
+
+ std::string htmlContainerId = std::string("container") + std::to_string(windowNum);
+ val htmlContainer =
+ val::global("document").call("getElementById", htmlContainerId);
+ std::string emCanvasSel = std::string("#canvas") + std::to_string(windowNum);
+
+ windowNum++;
+ return std::make_shared(htmlContainer, emCanvasSel);
+}
+
+//-----------------------------------------------------------------------------
+// 3DConnexion support
+//-----------------------------------------------------------------------------
+
+void Open3DConnexion() {}
+void Close3DConnexion() {}
+void Request3DConnexionEventsForWindow(WindowRef window) {}
+
+//-----------------------------------------------------------------------------
+// Message dialogs
+//-----------------------------------------------------------------------------
+
+class MessageDialogImplHtml;
+
+static std::vector> dialogsOnScreen;
+
+class MessageDialogImplHtml final : public MessageDialog,
+ public std::enable_shared_from_this {
+public:
+ val htmlModal;
+ val htmlDialog;
+ val htmlMessage;
+ val htmlDescription;
+ val htmlButtons;
+
+ std::vector> responseFuncs;
+
+ bool is_shown = false;
+
+ Response latestResponse = Response::NONE;
+
+ MessageDialogImplHtml() :
+ htmlModal(val::global("document").call("createElement", val("div"))),
+ htmlDialog(val::global("document").call("createElement", val("div"))),
+ htmlMessage(val::global("document").call("createElement", val("strong"))),
+ htmlDescription(val::global("document").call("createElement", val("p"))),
+ htmlButtons(val::global("document").call("createElement", val("div")))
+ {
+ htmlModal["classList"].call("add", val("modal"));
+ htmlModal.call("appendChild", htmlDialog);
+ htmlDialog["classList"].call("add", val("dialog"));
+ htmlDialog.call("appendChild", htmlMessage);
+ htmlDialog.call("appendChild", htmlDescription);
+ htmlButtons["classList"].call("add", val("buttons"));
+ htmlDialog.call("appendChild", htmlButtons);
+ }
+
+ void SetType(Type type) {
+ // FIXME(emscripten): implement
+ }
+
+ void SetTitle(std::string title) {
+ // FIXME(emscripten): implement
+ }
+
+ void SetMessage(std::string message) {
+ htmlMessage.set("innerText", Wrap(message));
+ }
+
+ void SetDescription(std::string description) {
+ htmlDescription.set("innerText", Wrap(description));
+ }
+
+ void AddButton(std::string label, Response response, bool isDefault = false) {
+ val htmlButton = val::global("document").call("createElement", val("div"));
+ htmlButton["classList"].call("add", val("button"));
+ val::global("window").call("setLabelWithMnemonic", htmlButton, Wrap(label));
+ if(isDefault) {
+ htmlButton["classList"].call("add", val("default"), val("selected"));
+ }
+
+ std::function responseFunc = [this, response] {
+ htmlModal.call("remove");
+ this->latestResponse = response;
+ if(onResponse) {
+ onResponse(response);
+ }
+ auto it = std::remove(dialogsOnScreen.begin(), dialogsOnScreen.end(),
+ shared_from_this());
+ dialogsOnScreen.erase(it);
+
+ this->is_shown = false;
+ };
+ if (responseFuncs.size() == 0) {
+ //FIXME(emscripten): I don't know why but the item in the head of responseFuncs cannot call.
+ // So add dummy item
+ responseFuncs.push_back([]{ });
+ }
+ responseFuncs.push_back(responseFunc);
+ std::function* callback = &responseFuncs.back();
+ htmlButton.call("addEventListener", val("trigger"), Wrap(callback));
+
+ htmlButtons.call("appendChild", htmlButton);
+ }
+
+ Response RunModal() {
+ this->ShowModal();
+ //FIXME(emscripten): use val::await() with JavaScript's Promise
+ while (true) {
+ if (this->is_shown) {
+ emscripten_sleep(50);
+ } else {
+ break;
+ }
+ }
+
+ if (this->latestResponse != Response::NONE) {
+ return this->latestResponse;
+ } else {
+ // FIXME(emscripten):
+ return this->latestResponse;
+ }
+ }
+
+ void ShowModal() {
+ dialogsOnScreen.push_back(shared_from_this());
+ val::global("document")["body"].call("appendChild", htmlModal);
+
+ this->is_shown = true;
+ }
+};
+
+MessageDialogRef CreateMessageDialog(WindowRef parentWindow) {
+ return std::make_shared();
+}
+
+//-----------------------------------------------------------------------------
+// File dialogs
+//-----------------------------------------------------------------------------
+
+// In emscripten psuedo filesystem, all userdata will be stored in this directory.
+static std::string basePathInFilesystem = "/data/";
+
+
+/* FileDialog that can open, save and browse. Also refer `src/platform/html/filemanagerui.js`.
+ */
+class FileDialogImplHtml : public FileDialog {
+public:
+
+ enum class Modes {
+ OPEN = 0,
+ SAVE,
+ BROWSER
+ };
+
+ Modes mode;
+
+ std::string title;
+ std::string filename;
+ std::string filters;
+
+ val jsFileManagerUI;
+
+ FileDialogImplHtml(Modes mode) {
+ dbp("FileDialogImplHtml::FileDialogImplHtml()");
+ val fileManagerUIClass = val::global("window")["FileManagerUI"];
+ val dialogModeValue;
+ this->mode = mode;
+ if (mode == Modes::OPEN) {
+ dialogModeValue = val(0);
+ } else if (mode == Modes::SAVE) {
+ dialogModeValue = val(1);
+ } else {
+ dialogModeValue = val(2);
+ }
+ this->jsFileManagerUI = fileManagerUIClass.new_(dialogModeValue);
+ dbp("FileDialogImplHtml::FileDialogImplHtml() Done.");
+ }
+
+ ~FileDialogImplHtml() override {
+ dbp("FileDialogImplHtml::~FileDialogImplHtml()");
+ this->jsFileManagerUI.call("dispose");
+ }
+
+ void SetTitle(std::string title) override {
+ dbp("FileDialogImplHtml::SetTitle(): title=\"%s\"", title.c_str());
+ this->title = title;
+ this->jsFileManagerUI.call("setTitle", val(title));
+ }
+
+ void SetCurrentName(std::string name) override {
+ dbp("FileDialogImplHtml::SetCurrentName(): name=\"%s\", parent=\"%s\"", name.c_str(), this->GetFilename().Parent().raw.c_str());
+
+ Path filepath = Path::From(name);
+ if (filepath.IsAbsolute()) {
+ // dbp("FileDialogImplHtml::SetCurrentName(): path is absolute.");
+ SetFilename(filepath);
+ } else {
+ // dbp("FileDialogImplHtml::SetCurrentName(): path is relative.");
+ SetFilename(GetFilename().Parent().Join(name));
+ }
+ }
+
+ Platform::Path GetFilename() override {
+ return Platform::Path::From(this->filename.c_str());
+ }
+
+ void SetFilename(Platform::Path path) override {
+ dbp("FileDialogImplHtml::GetFilename(): path=\"%s\"", path.raw.c_str());
+ this->filename = std::string(path.raw);
+ std::string filename_ = Path::From(this->filename).FileName();
+ this->jsFileManagerUI.call("setDefaultFilename", val(filename_));
+ }
+
+ void SuggestFilename(Platform::Path path) override {
+ dbp("FileDialogImplHtml::SuggestFilename(): path=\"%s\"", path.raw.c_str());
+ SetFilename(Platform::Path::From(path.FileStem()));
+ }
+
+ void AddFilter(std::string name, std::vector extensions) override {
+ if (this->filters.length() > 0) {
+ this->filters += ",";
+ }
+ for (size_t i = 0; i < extensions.size(); i++) {
+ if (i != 0) {
+ this->filters += ",";
+ }
+ this->filters += "." + extensions[i];
+ }
+ dbp("FileDialogImplHtml::AddFilter(): filter=%s", this->filters.c_str());
+ this->jsFileManagerUI.call("setFilter", val(this->filters));
+ }
+
+ void FreezeChoices(SettingsRef settings, const std::string &key) override {
+
+ }
+
+ void ThawChoices(SettingsRef settings, const std::string &key) override {
+ //FIXME(emscripten): implement
+ }
+
+ bool RunModal() override {
+ dbp("FileDialogImplHtml::RunModal()");
+
+ this->jsFileManagerUI.call("setBasePath", val(basePathInFilesystem));
+ this->jsFileManagerUI.call("show");
+ while (true) {
+ bool isShown = this->jsFileManagerUI.call("isShown");
+ if (!isShown) {
+ break;
+ } else {
+ emscripten_sleep(50);
+ }
+ }
+
+ dbp("FileSaveDialogImplHtml::RunModal() : dialog closed.");
+
+ std::string selectedFilename = this->jsFileManagerUI.call("getSelectedFilename");
+ if (selectedFilename.length() > 0) {
+ // Dummy call to set parent directory
+ this->SetFilename(Path::From(basePathInFilesystem + "/dummy"));
+ this->SetCurrentName(selectedFilename);
+ }
+
+
+ if (selectedFilename.length() > 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+};
+
+FileDialogRef CreateOpenFileDialog(WindowRef parentWindow) {
+ dbp("CreateOpenFileDialog()");
+ return std::shared_ptr(new FileDialogImplHtml(FileDialogImplHtml::Modes::OPEN));
+}
+
+FileDialogRef CreateSaveFileDialog(WindowRef parentWindow) {
+ dbp("CreateSaveFileDialog()");
+ return std::shared_ptr(new FileDialogImplHtml(FileDialogImplHtml::Modes::SAVE));
+}
+
+//-----------------------------------------------------------------------------
+// Application-wide APIs
+//-----------------------------------------------------------------------------
+
+std::vector GetFontFiles() {
+ return {};
+}
+
+void OpenInBrowser(const std::string &url) {
+ val::global("window").call("open", Wrap(url));
+}
+
+
+void OnSaveFinishedCallback(const Platform::Path& filename, bool is_saveAs, bool is_autosave) {
+ dbp("OnSaveFinished(): %s, is_saveAs=%d, is_autosave=%d\n", filename.FileName().c_str(), is_saveAs, is_autosave);
+ std::string filename_str = filename.raw;
+ EM_ASM(saveFileDone(UTF8ToString($0), $1, $2), filename_str.c_str(), is_saveAs, is_autosave);
+}
+
+std::vector InitGui(int argc, char **argv) {
+ static std::function onBeforeUnload = std::bind(&SolveSpaceUI::Exit, &SS);
+ val::global("window").call("addEventListener", val("beforeunload"),
+ Wrap(&onBeforeUnload));
+
+ // dbp("Set onSaveFinished");
+ SS.OnSaveFinished = OnSaveFinishedCallback;
+
+ // FIXME(emscripten): get locale from user preferences
+ SetLocale("en_US");
+
+ return {};
+}
+
+static void MainLoopIteration() {
+ // We don't do anything here, as all our work is registered via timers.
+}
+
+void RunGui() {
+ emscripten_set_main_loop(MainLoopIteration, 0, /*simulate_infinite_loop=*/true);
+}
+
+void ExitGui() {
+ exit(0);
+}
+
+void ClearGui() {}
+
+}
+}
diff --git a/src/platform/guimac.mm b/src/platform/guimac.mm
index 91a197d3..c560a5ac 100644
--- a/src/platform/guimac.mm
+++ b/src/platform/guimac.mm
@@ -372,7 +372,8 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
double rotationGestureCurrent;
Point2d trackpadPositionShift;
bool inTrackpadScrollGesture;
- int numTouches;
+ int activeTrackpadTouches;
+ bool scrollFromTrackpadTouch;
Platform::Window::Kind kind;
}
@@ -398,7 +399,8 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
editor.action = @selector(didEdit:);
inTrackpadScrollGesture = false;
- numTouches = 0;
+ activeTrackpadTouches = 0;
+ scrollFromTrackpadTouch = false;
self.acceptsTouchEvents = YES;
kind = aKind;
if(kind == Platform::Window::Kind::TOPLEVEL) {
@@ -576,9 +578,16 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
using Platform::MouseEvent;
MouseEvent event = [self convertMouseEvent:nsEvent];
- // Check for number of touches to exclude single-finger scrolling on Magic Mouse
- bool isTrackpadEvent = numTouches >= 2 && nsEvent.subtype == NSEventSubtypeTabletPoint;
- if(isTrackpadEvent && kind == Platform::Window::Kind::TOPLEVEL) {
+ if(nsEvent.phase == NSEventPhaseBegan) {
+ // If this scroll began on trackpad then touchesBeganWithEvent was called prior to this
+ // event and we have at least one active trackpad touch. We store this information so we
+ // can handle scroll originating from trackpad differently below.
+ scrollFromTrackpadTouch = activeTrackpadTouches > 0 &&
+ nsEvent.subtype == NSEventSubtypeTabletPoint &&
+ kind == Platform::Window::Kind::TOPLEVEL;
+ }
+ // Check if we are scrolling on trackpad and handle things differently.
+ if(scrollFromTrackpadTouch) {
// This is how Cocoa represents 2 finger trackpad drag gestures, rather than going via
// NSPanGestureRecognizer which is how you might expect this to work... We complicate this
// further by also handling shift-two-finger-drag to mean rotate. Fortunately we're using
@@ -632,20 +641,15 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
}
- (void)touchesBeganWithEvent:(NSEvent *)event {
- numTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count;
- [super touchesBeganWithEvent:event];
-}
-- (void)touchesMovedWithEvent:(NSEvent *)event {
- numTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count;
- [super touchesMovedWithEvent:event];
+ activeTrackpadTouches++;
}
+
- (void)touchesEndedWithEvent:(NSEvent *)event {
- numTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count;
- [super touchesEndedWithEvent:event];
+ activeTrackpadTouches--;
}
+
- (void)touchesCancelledWithEvent:(NSEvent *)event {
- numTouches = 0;
- [super touchesCancelledWithEvent:event];
+ activeTrackpadTouches--;
}
- (void)mouseExited:(NSEvent *)nsEvent {
@@ -983,10 +987,10 @@ public:
return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f;
}
- int GetDevicePixelRatio() override {
+ double GetDevicePixelRatio() override {
NSSize unitSize = { 1.0f, 0.0f };
unitSize = [ssView convertSizeToBacking:unitSize];
- return (int)unitSize.width;
+ return unitSize.width;
}
bool IsVisible() override {
diff --git a/src/platform/guiwin.cpp b/src/platform/guiwin.cpp
index ba9e18cf..e8ab2c1b 100644
--- a/src/platform/guiwin.cpp
+++ b/src/platform/guiwin.cpp
@@ -793,7 +793,7 @@ public:
break;
case WM_SIZING: {
- int pixelRatio = window->GetDevicePixelRatio();
+ double pixelRatio = window->GetDevicePixelRatio();
RECT rcw, rcc;
sscheck(GetWindowRect(window->hWindow, &rcw));
@@ -806,10 +806,10 @@ public:
int adjHeight = rc->bottom - rc->top;
adjWidth -= nonClientWidth;
- adjWidth = max(window->minWidth * pixelRatio, adjWidth);
- adjWidth += nonClientWidth;
+ adjWidth = max((int)(window->minWidth * pixelRatio), adjWidth);
+ adjWidth += nonClientWidth;
adjHeight -= nonClientHeight;
- adjHeight = max(window->minHeight * pixelRatio, adjHeight);
+ adjHeight = max((int)(window->minHeight * pixelRatio), adjHeight);
adjHeight += nonClientHeight;
switch(wParam) {
case WMSZ_RIGHT:
@@ -868,7 +868,7 @@ public:
case WM_MOUSEMOVE:
case WM_MOUSEWHEEL:
case WM_MOUSELEAVE: {
- int pixelRatio = window->GetDevicePixelRatio();
+ double pixelRatio = window->GetDevicePixelRatio();
MouseEvent event = {};
event.x = GET_X_LPARAM(lParam) / pixelRatio;
@@ -941,7 +941,7 @@ public:
event.y = pt.y / pixelRatio;
event.type = MouseEvent::Type::SCROLL_VERT;
- event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA;
+ event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) / (double)WHEEL_DELTA;
break;
case WM_MOUSELEAVE:
@@ -1109,10 +1109,10 @@ public:
return (double)dpi;
}
- int GetDevicePixelRatio() override {
+ double GetDevicePixelRatio() override {
UINT dpi;
sscheck(dpi = ssGetDpiForWindow(hWindow));
- return dpi / USER_DEFAULT_SCREEN_DPI;
+ return (double)dpi / USER_DEFAULT_SCREEN_DPI;
}
bool IsVisible() override {
@@ -1177,27 +1177,27 @@ public:
}
void GetContentSize(double *width, double *height) override {
- int pixelRatio = GetDevicePixelRatio();
+ double pixelRatio = GetDevicePixelRatio();
RECT rc;
sscheck(GetClientRect(hWindow, &rc));
- *width = (rc.right - rc.left) / pixelRatio;
- *height = (rc.bottom - rc.top) / pixelRatio;
+ *width = (rc.right - rc.left) / pixelRatio;
+ *height = (rc.bottom - rc.top) / pixelRatio;
}
void SetMinContentSize(double width, double height) {
minWidth = (int)width;
minHeight = (int)height;
- int pixelRatio = GetDevicePixelRatio();
+ double pixelRatio = GetDevicePixelRatio();
RECT rc;
sscheck(GetClientRect(hWindow, &rc));
if(rc.right - rc.left < minWidth * pixelRatio) {
- rc.right = rc.left + minWidth * pixelRatio;
+ rc.right = rc.left + (LONG)(minWidth * pixelRatio);
}
if(rc.bottom - rc.top < minHeight * pixelRatio) {
- rc.bottom = rc.top + minHeight * pixelRatio;
+ rc.bottom = rc.top + (LONG)(minHeight * pixelRatio);
}
}
@@ -1270,7 +1270,7 @@ public:
tooltipText = newText;
if(!newText.empty()) {
- int pixelRatio = GetDevicePixelRatio();
+ double pixelRatio = GetDevicePixelRatio();
RECT toolRect;
toolRect.left = (int)(x * pixelRatio);
toolRect.top = (int)(y * pixelRatio);
@@ -1301,9 +1301,9 @@ public:
bool isMonospace, const std::string &text) override {
if(IsEditorVisible()) return;
- int pixelRatio = GetDevicePixelRatio();
+ double pixelRatio = GetDevicePixelRatio();
- HFONT hFont = CreateFontW(-(LONG)fontHeight * GetDevicePixelRatio(), 0, 0, 0,
+ HFONT hFont = CreateFontW(-(int)(fontHeight * GetDevicePixelRatio()), 0, 0, 0,
FW_REGULAR, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial");
if(hFont == NULL) {
@@ -1324,12 +1324,12 @@ public:
sscheck(ReleaseDC(hEditor, hDc));
RECT rc;
- rc.left = (LONG)x * pixelRatio;
- rc.top = (LONG)y * pixelRatio - tm.tmAscent;
+ rc.left = (LONG)(x * pixelRatio);
+ rc.top = (LONG)(y * pixelRatio) - tm.tmAscent;
// Add one extra char width to avoid scrolling.
- rc.right = (LONG)x * pixelRatio +
- std::max((LONG)minWidth * pixelRatio, ts.cx + tm.tmAveCharWidth);
- rc.bottom = (LONG)y * pixelRatio + tm.tmDescent;
+ rc.right = (LONG)(x * pixelRatio) +
+ std::max((LONG)(minWidth * pixelRatio), ts.cx + tm.tmAveCharWidth);
+ rc.bottom = (LONG)(y * pixelRatio) + tm.tmDescent;
sscheck(ssAdjustWindowRectExForDpi(&rc, 0, /*bMenu=*/FALSE, WS_EX_CLIENTEDGE,
ssGetDpiForWindow(hWindow)));
@@ -1608,7 +1608,7 @@ public:
void AddFilter(std::string name, std::vector extensions) override {
std::string desc, patterns;
- for(auto extension : extensions) {
+ for(auto &extension : extensions) {
std::string pattern = "*." + extension;
if(!desc.empty()) desc += ", ";
desc += pattern;
diff --git a/src/platform/html/emshell.html b/src/platform/html/emshell.html
new file mode 100644
index 00000000..951d588e
--- /dev/null
+++ b/src/platform/html/emshell.html
@@ -0,0 +1,91 @@
+
+SolveSpace Web Edition (EXPERIMENTAL)
+
+
+
Downloading...
+
+ SolveSpace has crashed. See console for details.
+ The Web Edition of SolveSpace is experimental,
+ and may not be as reliable as the Desktop Edition.
+
Restart
+
+
+
+
{{{ SCRIPT }}}
diff --git a/src/platform/html/filemanagerui.js b/src/platform/html/filemanagerui.js
new file mode 100644
index 00000000..f82499b0
--- /dev/null
+++ b/src/platform/html/filemanagerui.js
@@ -0,0 +1,525 @@
+"use strict";
+
+const FileManagerUI_OPEN = 0;
+const FileManagerUI_SAVE = FileManagerUI_OPEN + 1;
+const FileManagerUI_BROWSE = FileManagerUI_SAVE + 1;
+
+//FIXME(emscripten): File size thresholds. How large file can we accept safely ?
+
+/** Maximum filesize for a uploaded file.
+ * @type {number} */
+const FileManagerUI_UPLOAD_FILE_SIZE_LIMIT = 50 * 1000 * 1000;
+
+const tryMakeDirectory = (path) => {
+ try {
+ FS.mkdir(path);
+ } catch {
+ // NOP
+ }
+}
+
+
+class FileManagerUI {
+ /**
+ * @param {number} mode - dialog mode FileManagerUI_[ OPEN, SAVE, BROWSE ]
+ */
+ constructor(mode) {
+ /** @type {boolean} */
+ this.__isOpenDialog = false;
+ /** @type {boolean} */
+ this.__isSaveDialog = false;
+ /** @type {boolean} */
+ this.__isBrowseDialog = false;
+
+ if (mode == FileManagerUI_OPEN) {
+ this.__isOpenDialog = true;
+ } else if (mode == FileManagerUI_SAVE) {
+ this.__isSaveDialog = true;
+ } else {
+ this.__isBrowseDialog = true;
+ }
+
+ /** @type {boolean} true if the dialog is shown. */
+ this.__isShown = false;
+
+ /** @type {string[]} */
+ this.__extension_filters = [".slvs"];
+
+ /** @type {string} */
+ this.__basePathInFilesystem = "";
+
+ /** @type {string} filename user selected. empty if nothing selected */
+ this.__selectedFilename = "";
+
+ this.__closedWithCancel = false;
+
+ this.__defaultFilename = "untitled";
+ }
+
+ /** deconstructor
+ */
+ dispose() {
+ if (this.__dialogRootElement) {
+ this.__dialogHeaderElement = null;
+ this.__descriptionElement = null;
+ this.__filelistElement = null;
+ this.__fileInputElement = null;
+ this.__saveFilenameInputElement = null;
+ this.__buttonContainerElement = null;
+ this.__dialogRootElement.parentElement.removeChild(this.__dialogRootElement);
+ this.__dialogRootElement = null;
+ }
+ }
+
+ /**
+ * @param {string} label
+ * @param {string} response
+ * @param {bool} isDefault
+ */
+ __addButton(label, response, isDefault, onclick) {
+ const buttonElem = document.createElement("div");
+ addClass(buttonElem, "button");
+ setLabelWithMnemonic(buttonElem, label);
+ if (isDefault) {
+ addClass(buttonElem, "default");
+ addClass(buttonElem, "selected");
+ }
+ buttonElem.addEventListener("click", () => {
+ if (onclick) {
+ if (onclick()) {
+ this.__close();
+ }
+ } else {
+ this.__close();
+ }
+ });
+
+ this.__buttonContainerElement.appendChild(buttonElem);
+ }
+
+ /**
+ * @param {HTMLElement} div element that built
+ */
+ buildDialog() {
+ const root = document.createElement('div');
+ addClass(root, "modal");
+ root.style.display = "none";
+ root.style.zIndex = 1000;
+
+ const dialog = document.createElement('div');
+ addClass(dialog, "dialog");
+ addClass(dialog, "wide");
+ root.appendChild(dialog);
+
+ const messageHeader = document.createElement('strong');
+ this.__dialogHeaderElement = messageHeader;
+ addClass(messageHeader, "dialog_header");
+ dialog.appendChild(messageHeader);
+
+ const description = document.createElement('p');
+ this.__descriptionElement = description;
+ dialog.appendChild(description);
+
+ const filelistheader = document.createElement('h3');
+ filelistheader.textContent = 'Files:';
+ dialog.appendChild(filelistheader);
+
+ const filelist = document.createElement('ul');
+ this.__filelistElement = filelist;
+ addClass(filelist, 'filelist');
+ dialog.appendChild(filelist);
+
+ const dummyfilelistitem = document.createElement('li');
+ dummyfilelistitem.textContent = "(No file in psuedo filesystem)";
+ filelist.appendChild(dummyfilelistitem);
+
+ if (this.__isOpenDialog) {
+ const fileuploadcontainer = document.createElement('div');
+ dialog.appendChild(fileuploadcontainer);
+
+ const fileuploadheader = document.createElement('h3');
+ fileuploadheader.textContent = "Upload file:";
+ fileuploadcontainer.appendChild(fileuploadheader);
+
+ const dragdropdescription = document.createElement('p');
+ dragdropdescription.textContent = "(Drag & drop file to the following box)";
+ dragdropdescription.style.fontSize = "0.8em";
+ dragdropdescription.style.margin = "0.1em";
+ fileuploadcontainer.appendChild(dragdropdescription);
+
+ const filedroparea = document.createElement('div');
+ addClass(filedroparea, 'filedrop');
+ filedroparea.addEventListener('dragstart', (ev) => this.__onFileDragDrop(ev));
+ filedroparea.addEventListener('dragover', (ev) => this.__onFileDragDrop(ev));
+ filedroparea.addEventListener('dragleave', (ev) => this.__onFileDragDrop(ev));
+ filedroparea.addEventListener('drop', (ev) => this.__onFileDragDrop(ev));
+ fileuploadcontainer.appendChild(filedroparea);
+
+ const fileinput = document.createElement('input');
+ this.__fileInputElement = fileinput;
+ fileinput.setAttribute('type', 'file');
+ fileinput.style.width = "100%";
+ fileinput.addEventListener('change', (ev) => this.__onFileInputChanged(ev));
+ filedroparea.appendChild(fileinput);
+
+ } else if (this.__isSaveDialog) {
+ const filenameinputcontainer = document.createElement('div');
+ dialog.appendChild(filenameinputcontainer);
+
+ const filenameinputheader = document.createElement('h3');
+ filenameinputheader.textContent = "Filename:";
+ filenameinputcontainer.appendChild(filenameinputheader);
+
+ const filenameinput = document.createElement('input');
+ filenameinput.setAttribute('type', 'input');
+ filenameinput.style.width = "90%";
+ filenameinput.style.margin = "auto 1em auto 1em";
+ this.__saveFilenameInputElement = filenameinput;
+ filenameinputcontainer.appendChild(filenameinput);
+ }
+
+ // Paragraph element for spacer
+ dialog.appendChild(document.createElement('p'));
+
+ const buttoncontainer = document.createElement('div');
+ this.__buttonContainerElement = buttoncontainer;
+ addClass(buttoncontainer, "buttons");
+ dialog.appendChild(buttoncontainer);
+
+ this.__addButton('OK', 0, false, () => {
+ if (this.__isOpenDialog) {
+ let selectedFilename = null;
+ const fileitems = document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]');
+ Array.from(fileitems).forEach((radiobox) => {
+ if (radiobox.checked) {
+ selectedFilename = radiobox.parentElement.getAttribute('data-filename');
+ }
+ });
+ if (selectedFilename) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return true;
+ }
+ });
+
+ this.__addButton('Cancel', 1, true, () => {
+ this.__closedWithCancel = true;
+ return true;
+ });
+
+ return root;
+ }
+
+ /**
+ * @param {string} text
+ */
+ setTitle(text) {
+ this.__dialogHeaderText = text;
+ }
+
+ /**
+ * @param {string} text
+ */
+ setDescription(text) {
+ this.__descriptionText = text;
+ }
+
+ /**
+ * @param {string} path file prefix. (ex) 'tmp/' to '/tmp/filename.txt'
+ */
+ setBasePath(path) {
+ this.__basePathInFilesystem = path;
+ tryMakeDirectory(path);
+ }
+
+ /**
+ * @param {string} filename
+ */
+ setDefaultFilename(filename) {
+ this.__defaultFilename = filename;
+ }
+
+ /**
+ *
+ * @param {string} filter comma-separated extensions like ".slvs,.stl;."
+ */
+ setFilter(filter) {
+ const exts = filter.split(',');
+ this.__extension_filters = exts;
+ }
+
+ __buildFileEntry(filename) {
+ const lielem = document.createElement('li');
+ const label = document.createElement('label');
+ label.setAttribute('data-filename', filename);
+ lielem.appendChild(label);
+ const radiobox = document.createElement('input');
+ radiobox.setAttribute('type', 'radio');
+ if (!this.__isOpenDialog) {
+ radiobox.style.display = "none";
+ }
+ radiobox.setAttribute('name', 'filemanager_filelist');
+ label.appendChild(radiobox);
+ const filenametext = document.createTextNode(filename);
+ label.appendChild(filenametext);
+
+ return lielem;
+ }
+
+ /**
+ * @returns {string[]} filename array
+ */
+ __getFileEntries() {
+ const basePath = this.__basePathInFilesystem;
+ /** @type {any[]} */
+ const nodes = FS.readdir(basePath);
+ /** @type {string[]} */
+ const files = nodes.filter((nodename) => {
+ return FS.isFile(FS.lstat(basePath + nodename).mode);
+ });
+ /*.map((filename) => {
+ return basePath + filename;
+ });*/
+ console.log(`__getFileEntries():`, files);
+ return files;
+ }
+
+ /**
+ * @param {string[]?} files file list already constructed
+ * @returns {string[]} filename array
+ */
+ __getFileEntries_recurse(basePath) {
+ //FIXME:remove try catch block
+ try {
+ //const basePath = this.__basePathInFilesystem;
+ FS.currentPath = basePath;
+ /** @type {any[]} */
+ const nodes = FS.readdir(basePath);
+
+ const filesInThisDirectory = nodes.filter((nodename) => {
+ return FS.isFile(FS.lstat(basePath + "/" + nodename).mode);
+ }).map((filename) => {
+ return basePath + "/" + filename;
+ });
+ let files = filesInThisDirectory;
+
+ const directories = nodes.filter((nodename) => {
+ return FS.isDir(FS.lstat(basePath + "/" + nodename).mode);
+ });
+
+ for (let i = 0; i < directories.length; i++) {
+ const directoryname = directories[i];
+ if (directoryname == '.' || directoryname == '..') {
+ continue;
+ }
+ const orig_cwd = FS.currentPath;
+ const directoryfullpath = basePath + "/" + directoryname;
+ FS.currentPath = directoryfullpath;
+ files = files.concat(this.__getFileEntries_recurse(directoryfullpath));
+ FS.currentPath = orig_cwd;
+ }
+
+ console.log(`__getFileEntries_recurse(): in "${basePath}"`, files);
+ return files;
+
+ } catch (excep) {
+ console.log(excep);
+ throw excep;
+ }
+ }
+
+ __updateFileList() {
+ console.log(`__updateFileList()`);
+ Array.from(this.__filelistElement.children).forEach((elem) => {
+ this.__filelistElement.removeChild(elem);
+ });
+ // const files = this.__getFileEntries();
+ FS.currentPath = this.__basePathInFilesystem;
+ const files = this.__getFileEntries_recurse(this.__basePathInFilesystem);
+ if (files.length < 1) {
+ const dummyfilelistitem = document.createElement('li');
+ dummyfilelistitem.textContent = "(No file in psuedo filesystem)";
+ this.__filelistElement.appendChild(dummyfilelistitem);
+
+ } else {
+ files.forEach((entry) => {
+ this.__filelistElement.appendChild(this.__buildFileEntry(entry));
+ });
+ }
+ }
+
+
+ /**
+ * @param {File} file
+ */
+ __getFileAsArrayBuffer(file) {
+ return new Promise((resolve, reject) => {
+ const filereader = new FileReader();
+ filereader.onerror = (ev) => {
+ reject(ev);
+ };
+ filereader.onload = (ev) => {
+ resolve(ev.target.result);
+ };
+ filereader.readAsArrayBuffer(file);
+ });
+ }
+
+ /**
+ *
+ * @param {File} file
+ */
+ async __tryAddFile(file) {
+ return new Promise(async (resolve, reject) => {
+ if (!file) {
+ reject(new Error(`Invalid arg: file is ${file}`));
+
+ } else if (file.size > FileManagerUI_UPLOAD_FILE_SIZE_LIMIT) {
+ //FIXME(emscripten): Use our MessageDialog instead of browser's alert().
+ alert(`Specified file is larger than limit of ${FileManagerUI_UPLOAD_FILE_SIZE_LIMIT} bytes. Canceced.`);
+ reject(new Error(`File is too large: "${file.name} is ${file.size} bytes`));
+
+ } else {
+ // Just add to Filesystem
+ const path = `${this.__basePathInFilesystem}${file.name}`;
+ const blobArrayBuffer = await this.__getFileAsArrayBuffer(file);
+ const u8array = new Uint8Array(blobArrayBuffer);
+ const fs = FS.open(path, "w");
+ FS.write(fs, u8array, 0, u8array.length, 0);
+ FS.close(fs);
+ resolve();
+ }
+ });
+ }
+
+ __addSelectedFile() {
+ if (this.__fileInputElement.files.length < 1) {
+ console.warn(`No file selected.`);
+ return;
+ }
+
+ const file = this.__fileInputElement.files[0];
+ this.__tryAddFile(file)
+ .then(() => {
+ this.__updateFileList();
+ })
+ .catch((err) => {
+ this.__fileInputElement.value = null;
+ console.error(err);
+ })
+ }
+
+ /**
+ * @param {DragEvent} ev
+ */
+ __onFileDragDrop(ev) {
+ ev.preventDefault();
+ if (ev.type == "dragenter" || ev.type == "dragover" || ev.type == "dragleave") {
+ return;
+ }
+ if (ev.dataTransfer.files.length < 1) {
+ return;
+ }
+ this.__fileInputElement.files = ev.dataTransfer.files;
+
+ this.__addSelectedFile();
+ }
+
+ /**
+ * @param {InputEvent} _ev
+ */
+ __onFileInputChanged(_ev) {
+ this.__addSelectedFile();
+ }
+
+
+ /** Show the FileManager UI dialog */
+ __show() {
+ this.__closedWithCancel = false;
+
+ /** @type {HTMLElement} */
+ this.__dialogRootElement = this.buildDialog();
+ document.querySelector('body').appendChild(this.__dialogRootElement);
+
+ this.__dialogHeaderElement.textContent = this.__dialogHeaderText || "File manager";
+ this.__descriptionElement.textContent = this.__descriptionText || "Select a file.";
+ if (this.__extension_filters) {
+ this.__descriptionElement.textContent += "Requested filter is " + this.__extension_filters.join(", ");
+ }
+
+ if (this.__isOpenDialog && this.__extension_filters) {
+ this.__fileInputElement.accept = this.__extension_filters.concat(',');
+ }
+
+ if (this.__isSaveDialog) {
+ this.__saveFilenameInputElement.value = this.__defaultFilename;
+ }
+
+ this.__dialogRootElement.style.display = "block";
+ this.__isShown = true;
+ }
+
+ /** Close the dialog */
+ __close() {
+ this.__selectedFilename = "";
+ if (this.__isOpenDialog) {
+ Array.from(document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]'))
+ .forEach((elem) => {
+ if (elem.checked) {
+ this.__selectedFilename = elem.parentElement.getAttribute("data-filename");
+ }
+ });
+ } else if (this.__isSaveDialog) {
+ if (!this.__closedWithCancel) {
+ this.__selectedFilename = this.__saveFilenameInputElement.value;
+ }
+ }
+
+ Array.from(this.__filelistElement.children).forEach((elem) => {
+ this.__filelistElement.removeChild(elem);
+ });
+
+ this.dispose();
+
+ this.__isShown = false;
+ }
+
+ /**
+ * @return {boolean}
+ */
+ isShown() {
+ return this.__isShown;
+ }
+
+ /**
+ *
+ * @returns {Promise} filename string on resolved.
+ */
+ showModalAsync() {
+ return new Promise((resolve, reject) => {
+ this.__show();
+ this.__updateFileList();
+ const intervalTimer = setInterval(() => {
+ if (!this.isShown()) {
+ clearInterval(intervalTimer);
+ resolve(this.__selectedFilename);
+ }
+ }, 50);
+ });
+ }
+
+ getSelectedFilename() {
+ return this.__selectedFilename;
+ }
+
+ show() {
+ this.__show();
+ this.__updateFileList();
+ }
+};
+
+
+window.FileManagerUI = FileManagerUI;
diff --git a/src/platform/html/solvespaceui.css b/src/platform/html/solvespaceui.css
new file mode 100644
index 00000000..3d2f44bb
--- /dev/null
+++ b/src/platform/html/solvespaceui.css
@@ -0,0 +1,344 @@
+* {
+ font-family: sans;
+}
+html, body {
+ padding: 0;
+ margin: 0;
+ background: black;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+html, body, canvas, #splash, #container {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+}
+body {
+ overflow: hidden;
+}
+
+/* Splashscreen */
+#splash {
+ z-index: 1000;
+ background: black;
+ color: white;
+ position: absolute;
+}
+#splash .center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+}
+#splash a {
+ color: white;
+}
+
+#spinner {
+ height: 30px;
+ width: 30px;
+ margin: 0px auto;
+ border-left: 10px solid rgb(255, 255, 255);
+ border-top: 10px solid rgb(0, 255, 0);
+ border-right: 10px solid rgb(255, 0, 255);
+ border-bottom: 10px solid rgb(0, 255, 0);
+ border-radius: 100%;
+ animation: rotation 3s linear infinite;
+ margin-bottom: 5px;
+}
+@keyframes rotation {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* Grid layout for main */
+main {
+ height: 100%;
+
+ /* Use CSS Grid layout for vertical placement. */
+ display: grid;
+ /* Row 0 for menubar (fit to content), Row 1 for canvas0, canvas1 (rest of space) */
+ grid-template-rows: auto 1fr;
+}
+
+/* Buttons */
+.button {
+ border: 1px solid hsl(0, 0%, 60%);
+ background: hsl(0, 0%, 10%);
+ color: white;
+ padding: 4px 8px;
+ cursor: default;
+}
+.button.selected {
+ background: hsl(0, 0%, 20%);
+}
+.button:hover {
+ background: hsl(0, 0%, 40%);
+}
+
+/* Editors */
+.editor {
+ position: fixed;
+ padding: 0;
+ border: none;
+}
+
+/* Menus */
+.menu {
+ font-size: 0;
+ margin: 0;
+ padding: 0;
+ padding-right: 10px;
+ list-style-type: none;
+ background: hsl(0, 0%, 20%);
+ color: white;
+ cursor: default;
+
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+/* Normal menu items */
+.menu > li {
+ z-index: 100;
+ font-size: 16px;
+ display: inline-flex;
+ justify-content: space-between;
+ align-items: center;
+ white-space: nowrap;
+ position: relative;
+ width: 100%;
+ height: 19px;
+ margin: 2px;
+ padding: 3px;
+}
+.menu > li::before, .menu > li::after {
+ font-family: 'Font Awesome 5 Free';
+ font-weight: 900;
+ font-size: 12px;
+}
+.menu > li.hover,
+.menu > li.selected,
+.menu.menubar > li:hover:not(.selected) {
+ background: hsl(0, 0%, 30%);
+}
+.menu > li.disabled {
+ color: hsl(0, 0%, 30%);
+}
+
+/* Check and radio menu items */
+.menu > li {
+ padding-left: 24px;
+}
+.menu > li::before {
+ position: absolute;
+ text-align: center;
+ left: 0px;
+ width: 24px;
+}
+.menu > li.check::before {
+ content: '\f0c8';
+}
+.menu > li.check.active::before {
+ content: '\f14a';
+}
+.menu > li.radio::before {
+ content: '\f111';
+}
+.menu > li.radio.active::before {
+ content: '\f192';
+}
+
+/* Separator menu items */
+.menu > li.separator {
+ height: 0px;
+ border-top: 1px solid hsl(0, 0%, 30%);
+ margin: 0 2px 0 2px;
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+/* Accelerators */
+.menu > li > .accel {
+ text-align: right;
+ margin-left: 20px;
+}
+
+/* Submenus */
+.menu > li > .menu,
+.menu.popup {
+ display: none;
+ white-space: normal;
+ padding-right: 31px;
+}
+.menu > li.has-submenu::after {
+ content: '\f0da';
+}
+.menu > li.selected > .menu,
+.menu > li.hover > .menu,
+.menu.popup {
+ display: block;
+ background: hsl(0, 0%, 10%);
+ border: 1px solid hsl(0, 0%, 30%);
+ position: absolute;
+ left: 100%;
+ top: -3px;
+}
+
+/* Popup menus */
+.menu.popup {
+ display: block;
+ position: absolute;
+ width: min-content;
+}
+
+/* Menubars */
+.menubar {
+ padding-left: 5px;
+}
+.menubar > li {
+ width: auto;
+ width: fit-content;
+ margin: 0;
+ padding: 5px;
+}
+.menubar > li.selected {
+ background: hsl(0, 0%, 10%);
+ border: 1px solid hsl(0, 0%, 30%);
+ padding: 4px;
+}
+.menubar.menu > li.selected > .menu {
+ display: block;
+ position: absolute;
+ left: -1px;
+ top: 27px;
+}
+
+/* Modal popups */
+.modal {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background: hsla(0, 0%, 0%, 60%);
+}
+.modal > div {
+ position: absolute;
+ top: 15%;
+ left: 50%;
+ transform: translate(-50%, 0%);
+}
+
+/* Dialogs */
+.dialog {
+ border: 1px solid hsl(0, 0%, 30%);
+ background: hsl(0, 0%, 10%);
+ color: white;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ min-width: 200px;
+ max-width: 400px;
+ white-space: pre-wrap;
+ max-height: 70%;
+ overflow-y: auto;
+}
+.dialog.wide {
+ width: 80%;
+ max-width: 1200px;
+}
+.dialog > .buttons {
+ display: flex;
+ justify-content: space-around;
+}
+.dialog .filedrop {
+ margin: 1em 0 1em 0;
+ padding: 1em;
+ border: 2px solid black;
+ background-color: hsl(0, 0%, 50%);
+}
+.dialog .filelist {
+ display: flex;
+ flex-flow: row wrap;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+.dialog .filelist li {
+ padding: 0.2em 0.5em 0.2em 0.5em;
+ break-inside: avoid;
+}
+
+/* Mnemonics */
+.label > u {
+ position: relative;
+ top: 0px;
+ text-decoration: none;
+}
+body.mnemonic .label > u {
+ border-bottom: 1px solid;
+}
+
+/* Canvases */
+canvas {
+ border: 0px none;
+ background-color: black;
+}
+
+#container {
+ display: flex;
+ overflow: hidden;
+}
+/* FIXME(emscripten): this should be dynamically adjustable, not hardcoded in CSS */
+#container0 {
+ flex-basis: 80%;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+}
+
+#container1parent {
+ flex-basis: 20%;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+ min-width: 410px;
+
+ display: grid;
+ grid-template-columns: auto 19px;
+ grid-template-rows: 100%;
+}
+
+#container1 {
+ height: 100%;
+}
+
+#canvas1scrollbarbox {
+ /* 19px is a magic number for scrollbar width (Yes, this is platform-dependent value but looks almost working.) */
+ width: 19px;
+ min-width: 19px;
+ height: 100%;
+ overflow-x: hidden;
+ overflow-y: scroll;
+
+ background-color: lightgray;
+ -webkit-overflow-scrolling: auto;
+}
+
+#canvas1scrollbar {
+ /* 0px will disable the scrollbar by browser. */
+ width: 1px;
+ /* Disable scrollbar as default. This value will be overwritten by program. */
+ height: 100%;
+}
+
+#view_separator {
+ width: 4px;
+ background: hsl(0, 0%, 20%);
+}
diff --git a/src/platform/html/solvespaceui.js b/src/platform/html/solvespaceui.js
new file mode 100644
index 00000000..edcca32e
--- /dev/null
+++ b/src/platform/html/solvespaceui.js
@@ -0,0 +1,813 @@
+function isModal() {
+ var hasModal = !!document.querySelector('.modal');
+ var hasMenuBar = !!document.querySelector('.menubar .selected');
+ var hasPopupMenu = !!document.querySelector('.menu.popup');
+ return hasModal || hasMenuBar || hasPopupMenu;
+}
+
+/* String helpers */
+
+/**
+ * @param {string} s - original string
+ * @param {number} digits - char length of generating string
+ * @param {string} ch - string to be used for padding
+ * @return {string} generated string ($digits chars length) or $s
+ */
+function stringPadLeft(s, digits, ch) {
+ if (s.length > digits) {
+ return s;
+ }
+ for (let i = s.length; i < digits; i++) {
+ s = ch + s;
+ }
+ return s;
+}
+
+/** Generate a string expression of now
+ * @return {string} like a "2022_08_31_2245" string (for 2022-08-31 22:45; local time)
+ */
+function GetCurrentDateTimeString() {
+ const now = new Date();
+ const padLeft2 = (num) => { return stringPadLeft(num.toString(), 2, '0') };
+ return (`${now.getFullYear()}_${padLeft2(now.getMonth()+1)}_${padLeft2(now.getDate())}` +
+ `_` + `${padLeft2(now.getHours())}${padLeft2(now.getMinutes())}`);
+}
+
+/* CSS helpers */
+function hasClass(element, className) {
+ return element.classList.contains(className);
+}
+function addClass(element, className) {
+ element.classList.add(className);
+}
+function removeClass(element, className) {
+ element.classList.remove(className);
+}
+function removeClassFromAllChildren(element, className) {
+ element.querySelectorAll('.' + className).forEach(function(element) {
+ removeClass(element, className);
+ })
+}
+
+/* Mnemonic helpers */
+function setLabelWithMnemonic(element, labelText) {
+ var label = document.createElement('span');
+ addClass(label, 'label');
+ element.appendChild(label);
+
+ var matches = labelText.match('(.*?)&(.)(.*)?');
+ if(matches) {
+ label.appendChild(document.createTextNode(matches[1]));
+ if(matches[2]) {
+ var mnemonic = document.createElement('u');
+ mnemonic.innerText = matches[2];
+ label.appendChild(mnemonic);
+ addClass(element, 'mnemonic-Key' + matches[2].toUpperCase());
+ }
+ if(matches[3]) {
+ label.appendChild(document.createTextNode(matches[3]));
+ }
+ } else {
+ label.appendChild(document.createTextNode(labelText))
+ }
+}
+
+/** Touchevent helper
+ * @param {TouchEvent} event
+ * @return {boolean} true if same element is target of touchstart and touchend
+ */
+function isSameElementOnTouchstartAndTouchend(event) {
+ const elementOnTouchStart = event.target;
+ const elementOnTouchEnd = document.elementFromPoint(event.changedTouches[0].clientX, event.changedTouches[0].clientY);
+ return elementOnTouchStart == elementOnTouchEnd;
+}
+
+/* Button helpers */
+function isButton(element) {
+ return hasClass(element, 'button');
+}
+
+/* Button DOM traversal helpers */
+function getButton(element) {
+ if(!element) return;
+ if(element.tagName == 'U') {
+ element = element.parentElement;
+ }
+ if(hasClass(element, 'label')) {
+ return getButton(element.parentElement);
+ } else if(isButton(element)) {
+ return element;
+ }
+}
+
+/* Button behavior */
+window.addEventListener('click', function(event) {
+ var button = getButton(event.target);
+ if(button) {
+ button.dispatchEvent(new Event('trigger'));
+ }
+});
+window.addEventListener("touchend", (event) => {
+ if (!isSameElementOnTouchstartAndTouchend(event)) {
+ return;
+ }
+ const button = getButton(event.target);
+ if (button) {
+ button.dispatchEvent(new Event('trigger'));
+ }
+});
+
+window.addEventListener('keydown', function(event) {
+ var selected = document.querySelector('.button.selected');
+ if(!selected) return;
+
+ var outSelected, newSelected;
+ if(event.key == 'ArrowRight') {
+ outSelected = selected;
+ newSelected = selected.nextElementSibling;
+ if(!newSelected) {
+ newSelected = outSelected.parentElement.firstElementChild;
+ }
+ } else if(event.key == 'ArrowLeft') {
+ outSelected = selected;
+ newSelected = selected.previousElementSibling;
+ if(!newSelected) {
+ newSelected = outSelected.parentElement.lastElementChild;
+ }
+ } else if(event.key == 'Enter') {
+ selected.dispatchEvent(new Event('trigger'));
+ } else if(event.key == 'Escape' && hasClass(selected, 'default')) {
+ selected.dispatchEvent(new Event('trigger'));
+ }
+
+ if(outSelected) removeClass(outSelected, 'selected');
+ if(newSelected) addClass(newSelected, 'selected');
+
+ event.stopPropagation();
+});
+
+/* Editor helpers */
+function isEditor(element) {
+ return hasClass(element, 'editor');
+}
+
+/* Editor DOM traversal helpers */
+function getEditor(element) {
+ if(!element) return;
+ if(isEditor(element)) {
+ return element;
+ }
+}
+
+/* Editor behavior */
+window.addEventListener('keydown', function(event) {
+ var editor = getEditor(event.target);
+ if(editor) {
+ if(event.key == 'Enter') {
+ editor.dispatchEvent(new Event('trigger'));
+ } else if(event.key == 'Escape') {
+ editor.style.display = 'none';
+ }
+ event.stopPropagation();
+ }
+}, {capture: true});
+
+/* Menu helpers */
+function isMenubar(element) {
+ return hasClass(element, 'menubar');
+}
+function isMenu(element) {
+ return hasClass(element, 'menu');
+}
+function isPopupMenu(element) {
+ return isMenu(element) && hasClass(element, 'popup')
+}
+function hasSubmenu(menuItem) {
+ return !!menuItem.querySelector('.menu');
+}
+
+/* Menu item helpers */
+function isMenuItemSelectable(menuItem) {
+ return !(hasClass(menuItem, 'disabled') || hasClass(menuItem, 'separator'));
+}
+function isMenuItemSelected(menuItem) {
+ return hasClass(menuItem, 'selected') || hasClass(menuItem, 'hover');
+}
+function deselectMenuItem(menuItem) {
+ removeClass(menuItem, 'selected');
+ removeClass(menuItem, 'hover');
+ removeClassFromAllChildren(menuItem, 'selected');
+ removeClassFromAllChildren(menuItem, 'hover');
+}
+function selectMenuItem(menuItem) {
+ var menu = menuItem.parentElement;
+ removeClassFromAllChildren(menu, 'selected');
+ removeClassFromAllChildren(menu, 'hover');
+ if(isMenubar(menu)) {
+ addClass(menuItem, 'selected');
+ } else {
+ addClass(menuItem, 'hover');
+ }
+}
+function triggerMenuItem(menuItem) {
+ selectMenuItem(menuItem);
+ if(hasSubmenu(menuItem)) {
+ selectMenuItem(menuItem.querySelector('li:first-child'));
+ } else {
+ var parent = menuItem.parentElement;
+ while(!isMenubar(parent) && !isPopupMenu(parent)) {
+ parent = parent.parentElement;
+ }
+ removeClassFromAllChildren(parent, 'selected');
+ removeClassFromAllChildren(parent, 'hover');
+ if(isPopupMenu(parent)) {
+ parent.remove();
+ }
+
+ menuItem.dispatchEvent(new Event('trigger'));
+ }
+}
+
+/* Menu DOM traversal helpers */
+function getMenuItem(element) {
+ if(!element) return;
+ if(element.tagName == 'U') {
+ element = element.parentElement;
+ }
+ if(hasClass(element, 'label')) {
+ return getMenuItem(element.parentElement);
+ } else if(element.tagName == 'LI' && isMenu(element.parentElement)) {
+ return element;
+ }
+}
+function getMenu(element) {
+ if(!element) return;
+ if(isMenu(element)) {
+ return element;
+ } else {
+ var menuItem = getMenuItem(element);
+ if(menuItem && isMenu(menuItem.parentElement)) {
+ return menuItem.parentElement;
+ }
+ }
+}
+
+/* Menu behavior */
+window.addEventListener('click', function(event) {
+ var menuItem = getMenuItem(event.target);
+ var menu = getMenu(menuItem);
+ if(menu && isMenubar(menu)) {
+ if(hasClass(menuItem, 'selected')) {
+ removeClass(menuItem, 'selected');
+ } else {
+ selectMenuItem(menuItem);
+ }
+ event.stopPropagation();
+ } else if(menu) {
+ if(!hasSubmenu(menuItem)) {
+ triggerMenuItem(menuItem);
+ }
+ event.stopPropagation();
+ } else {
+ document.querySelectorAll('.menu .selected, .menu .hover')
+ .forEach(function(menuItem) {
+ deselectMenuItem(menuItem);
+ event.stopPropagation();
+ });
+ document.querySelectorAll('.menu.popup')
+ .forEach(function(menu) {
+ menu.remove();
+ });
+ }
+});
+window.addEventListener("touchend", (event) => {
+ if (!isSameElementOnTouchstartAndTouchend(event)) {
+ return;
+ }
+ var menuItem = getMenuItem(event.target);
+ var menu = getMenu(menuItem);
+ if(menu && isMenubar(menu)) {
+ if(hasClass(menuItem, 'selected')) {
+ removeClass(menuItem, 'selected');
+ } else {
+ selectMenuItem(menuItem);
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ } else if(menu) {
+ if(!hasSubmenu(menuItem)) {
+ triggerMenuItem(menuItem);
+ } else {
+ addClass(menuItem, "selected");
+ addClass(menuItem, "hover");
+ }
+ event.stopPropagation();
+ } else {
+ document.querySelectorAll('.menu .selected, .menu .hover')
+ .forEach(function(menuItem) {
+ deselectMenuItem(menuItem);
+ event.stopPropagation();
+ });
+ document.querySelectorAll('.menu.popup')
+ .forEach(function(menu) {
+ menu.remove();
+ });
+ }
+});
+window.addEventListener('mouseover', function(event) {
+ var menuItem = getMenuItem(event.target);
+ var menu = getMenu(menuItem);
+ if(menu) {
+ var selected = menu.querySelectorAll('.selected, .hover');
+ if(isMenubar(menu)) {
+ if(selected.length > 0) {
+ selected.forEach(function(menuItem) {
+ if(selected != menuItem) {
+ deselectMenuItem(menuItem);
+ }
+ });
+ addClass(menuItem, 'selected');
+ }
+ } else {
+ if(isMenuItemSelectable(menuItem)) {
+ selectMenuItem(menuItem);
+ }
+ }
+ }
+});
+window.addEventListener('keydown', function(event) {
+ var allSelected = document.querySelectorAll('.menubar .selected, .menubar .hover,' +
+ '.menu.popup .selected, .menu.popup .hover');
+ if(allSelected.length == 0) return;
+
+ var selected = allSelected[allSelected.length - 1];
+ var outSelected, newSelected;
+ var isMenubarItem = isMenubar(getMenu(selected));
+
+ if(isMenubarItem && event.key == 'ArrowRight' ||
+ !isMenubarItem && event.key == 'ArrowDown') {
+ outSelected = selected;
+ newSelected = selected.nextElementSibling;
+ while(newSelected && !isMenuItemSelectable(newSelected)) {
+ newSelected = newSelected.nextElementSibling;
+ }
+ if(!newSelected) {
+ newSelected = outSelected.parentElement.firstElementChild;
+ }
+ } else if(isMenubarItem && event.key == 'ArrowLeft' ||
+ !isMenubarItem && event.key == 'ArrowUp') {
+ outSelected = selected;
+ newSelected = selected.previousElementSibling;
+ while(newSelected && !isMenuItemSelectable(newSelected)) {
+ newSelected = newSelected.previousElementSibling;
+ }
+ if(!newSelected) {
+ newSelected = outSelected.parentElement.lastElementChild;
+ }
+ } else if(!isMenubarItem && event.key == 'ArrowRight') {
+ if(hasSubmenu(selected)) {
+ selectMenuItem(selected.querySelector('li:first-child'));
+ } else {
+ outSelected = allSelected[0];
+ newSelected = outSelected.nextElementSibling;
+ if(!newSelected) {
+ newSelected = outSelected.parentElement.firstElementChild;
+ }
+ }
+ } else if(!isMenubarItem && event.key == 'ArrowLeft') {
+ if(allSelected.length > 2) {
+ outSelected = selected;
+ } else {
+ outSelected = allSelected[0];
+ newSelected = outSelected.previousElementSibling;
+ if(!newSelected) {
+ newSelected = outSelected.parentElement.lastElementChild;
+ }
+ }
+ } else if(isMenubarItem && event.key == 'ArrowDown') {
+ newSelected = selected.querySelector('li:first-child');
+ } else if(event.key == 'Enter') {
+ triggerMenuItem(selected);
+ } else if(event.key == 'Escape') {
+ outSelected = allSelected[0];
+ } else {
+ var withMnemonic = getMenu(selected).querySelector('.mnemonic-' + event.key);
+ if(withMnemonic) {
+ triggerMenuItem(withMnemonic);
+ }
+ }
+
+ if(outSelected) deselectMenuItem(outSelected);
+ if(newSelected) selectMenuItem(newSelected);
+
+ event.stopPropagation();
+});
+
+/* Mnemonic behavior */
+window.addEventListener('keydown', function(event) {
+ var withMnemonic;
+ if(event.altKey && event.key == 'Alt') {
+ addClass(document.body, 'mnemonic');
+ } else if(!isModal() && event.altKey && (withMnemonic =
+ document.querySelector('.menubar > .mnemonic-' + event.code))) {
+ triggerMenuItem(withMnemonic);
+ event.stopPropagation();
+ } else {
+ removeClass(document.body, 'mnemonic');
+ }
+});
+window.addEventListener('keyup', function(event) {
+ if(event.key == 'Alt') {
+ removeClass(document.body, 'mnemonic');
+ }
+});
+
+
+// FIXME(emscripten): Should be implemnted in guihtmlcpp ?
+class FileUploadHelper {
+ constructor() {
+ this.modalRoot = document.createElement("div");
+ addClass(this.modalRoot, "modal");
+ this.modalRoot.style.display = "none";
+ this.modalRoot.style.zIndex = 1000;
+
+ this.dialogRoot = document.createElement("div");
+ addClass(this.dialogRoot, "dialog");
+ this.modalRoot.appendChild(this.dialogRoot);
+
+ this.messageHeader = document.createElement("strong");
+ this.dialogRoot.appendChild(this.messageHeader);
+
+ this.descriptionParagraph = document.createElement("p");
+ this.dialogRoot.appendChild(this.descriptionParagraph);
+
+ this.currentFileListHeader = document.createElement("p");
+ this.currentFileListHeader.textContent = "Current uploaded files:";
+ this.dialogRoot.appendChild(this.currentFileListHeader);
+
+ this.currentFileList = document.createElement("div");
+ this.dialogRoot.appendChild(this.currentFileList);
+
+ this.fileInputContainer = document.createElement("div");
+
+ this.fileInputElement = document.createElement("input");
+ this.fileInputElement.setAttribute("type", "file");
+ this.fileInputElement.addEventListener("change", (ev)=> this.onFileInputChanged(ev));
+ this.fileInputContainer.appendChild(this.fileInputElement);
+
+ this.dialogRoot.appendChild(this.fileInputContainer);
+
+ this.buttonHolder = document.createElement("div");
+ addClass(this.buttonHolder, "buttons");
+ this.dialogRoot.appendChild(this.buttonHolder);
+
+ this.AddButton("OK", 0, false);
+ this.AddButton("Cancel", 1, true);
+
+ this.closeDialog();
+
+ document.querySelector("body").appendChild(this.modalRoot);
+
+ this.currentFilename = null;
+
+ // FIXME(emscripten): For debugging
+ this.title = "";
+ this.filename = "";
+ this.filters = "";
+ }
+
+ dispose() {
+ document.querySelector("body").removeChild(this.modalRoot);
+ }
+
+ AddButton(label, response, isDefault) {
+ // FIXME(emscripten): implement
+ const buttonElem = document.createElement("div");
+ addClass(buttonElem, "button");
+ setLabelWithMnemonic(buttonElem, label);
+ if (isDefault) {
+ addClass(buttonElem, "default");
+ addClass(buttonElem, "selected");
+ }
+ buttonElem.addEventListener("click", () => {
+ this.closeDialog();
+ });
+
+ this.buttonHolder.appendChild(buttonElem);
+ }
+
+ getFileEntries() {
+ const basePath = '/';
+ /** @type {Array {
+ return FS.isFile(FS.lstat(basePath + nodename).mode);
+ }).map((filename) => {
+ return basePath + filename;
+ });
+ return files;
+ }
+
+ generateFileList() {
+ let filepaths = this.getFileEntries();
+ const listElem = document.createElement("ul");
+ for (let i = 0; i < filepaths.length; i++) {
+ const listitemElem = document.createElement("li");
+ const stat = FS.lstat(filepaths[i]);
+ const text = `"${filepaths[i]}" (${stat.size} bytes)`;
+ listitemElem.textContent = text;
+ listElem.appendChild(listitemElem);
+ }
+ return listElem;
+ }
+
+ updateFileList() {
+ this.currentFileList.innerHTML = "";
+ this.currentFileList.appendChild(this.generateFileList());
+ }
+
+ onFileInputChanged(ev) {
+ const selectedFiles = ev.target.files;
+ if (selectedFiles.length < 1) {
+ return;
+ }
+ const selectedFile = selectedFiles[0];
+ const selectedFilename = selectedFile.name;
+ this.filename = selectedFilename;
+ this.currentFilename = selectedFilename;
+
+ // Prepare FileReader
+ const fileReader = new FileReader();
+ const fileReaderReadAsArrayBufferPromise = new Promise((resolve, reject) => {
+ fileReader.addEventListener("load", (ev) => {
+ resolve(ev.target.result);
+ });
+ fileReader.addEventListener("abort", (err) => {
+ reject(err);
+ });
+ fileReader.readAsArrayBuffer(selectedFile);
+ });
+
+ fileReaderReadAsArrayBufferPromise
+ .then((arrayBuffer) => {
+ // Write selected file to FS
+ console.log(`Write uploaded file blob to filesystem. "${selectedFilename}" (${arrayBuffer.byteLength} bytes)`);
+ const u8array = new Uint8Array(arrayBuffer);
+ const fs = FS.open("/" + selectedFilename, "w");
+ FS.write(fs, u8array, 0, u8array.length, 0);
+ FS.close(fs);
+
+ // Update file list in dialog
+ this.updateFileList();
+ })
+ .catch((err) => {
+ console.error("Error while fileReader.readAsArrayBuffer():", err);
+ });
+ }
+
+ showDialog() {
+ this.updateFileList();
+
+ this.is_shown = true;
+ this.modalRoot.style.display = "block";
+ }
+
+ closeDialog() {
+ this.is_shown = false;
+ this.modalRoot.style.display = "none";
+ }
+};
+
+// FIXME(emscripten): Workaround
+function createFileUploadHelperInstance() {
+ return new FileUploadHelper();
+}
+
+// FIXME(emscripten): Should be implemnted in guihtmlcpp ?
+class FileDownloadHelper {
+ constructor() {
+ this.modalRoot = document.createElement("div");
+ addClass(this.modalRoot, "modal");
+ this.modalRoot.style.display = "none";
+ this.modalRoot.style.zIndex = 1000;
+
+ this.dialogRoot = document.createElement("div");
+ addClass(this.dialogRoot, "dialog");
+ this.modalRoot.appendChild(this.dialogRoot);
+
+ this.messageHeader = document.createElement("strong");
+ this.dialogRoot.appendChild(this.messageHeader);
+
+ this.descriptionParagraph = document.createElement("p");
+ this.dialogRoot.appendChild(this.descriptionParagraph);
+
+ this.buttonHolder = document.createElement("div");
+ addClass(this.buttonHolder, "buttons");
+ this.dialogRoot.appendChild(this.buttonHolder);
+
+ this.closeDialog();
+
+ document.querySelector("body").appendChild(this.modalRoot);
+ }
+
+ dispose() {
+ document.querySelector("body").removeChild(this.modalRoot);
+ }
+
+ AddButton(label, response, isDefault) {
+ // FIXME(emscripten): implement
+ const buttonElem = document.createElement("div");
+ addClass(buttonElem, "button");
+ setLabelWithMnemonic(buttonElem, label);
+ if (isDefault) {
+ addClass(buttonElem, "default");
+ addClass(buttonElem, "selected");
+ }
+ buttonElem.addEventListener("click", () => {
+ this.closeDialog();
+ this.dispose();
+ });
+
+ this.buttonHolder.appendChild(buttonElem);
+ }
+
+ createBlobURLFromArrayBuffer(arrayBuffer) {
+ const u8array = new Uint8Array(arrayBuffer);
+ let dataUrl = "data:application/octet-stream;base64,";
+ let binaryString = "";
+ for (let i = 0; i < u8array.length; i++) {
+ binaryString += String.fromCharCode(u8array[i]);
+ }
+ dataUrl += btoa(binaryString);
+
+ return dataUrl;
+ }
+
+ prepareFile(filename) {
+ this.messageHeader.textContent = "Your file ready";
+
+ const stat = FS.lstat(filename);
+ const filesize = stat.size;
+ const fs = FS.open(filename, "r");
+ const readbuffer = new Uint8Array(filesize);
+ FS.read(fs, readbuffer, 0, filesize, 0);
+ FS.close(fs);
+
+ const blobURL = this.createBlobURLFromArrayBuffer(readbuffer.buffer);
+
+ this.descriptionParagraph.innerHTML = "";
+ const linkElem = document.createElement("a");
+ //let downloadfilename = "solvespace_browser-";
+ //downloadfilename += `${GetCurrentDateTimeString()}.slvs`;
+ let downloadfilename = filename;
+ linkElem.setAttribute("download", downloadfilename);
+ linkElem.setAttribute("href", blobURL);
+ // WORKAROUND: FIXME(emscripten)
+ linkElem.style.color = "lightblue";
+ linkElem.textContent = downloadfilename;
+ this.descriptionParagraph.appendChild(linkElem);
+ }
+
+ showDialog() {
+ this.is_shown = true;
+ this.modalRoot.style.display = "block";
+ }
+
+ closeDialog() {
+ this.is_shown = false;
+ this.modalRoot.style.display = "none";
+ }
+};
+
+function saveFileDone(filename, isSaveAs, isAutosave) {
+ console.log(`saveFileDone(${filename}, ${isSaveAs}, ${isAutosave})`);
+ if (isAutosave) {
+ return;
+ }
+ const fileDownloadHelper = new FileDownloadHelper();
+ fileDownloadHelper.AddButton("OK", 0, true);
+ fileDownloadHelper.prepareFile(filename);
+ console.log(`Calling shoDialog()...`);
+ fileDownloadHelper.showDialog();
+ console.log(`shoDialog() finished.`);
+}
+
+
+class ScrollbarHelper {
+ /**
+ * @param {HTMLElement} elementquery CSS query string for the element that has scrollbar.
+ */
+ constructor(elementquery) {
+ this.target = document.querySelector(elementquery);
+ this.rangeMin = 0;
+ this.rangeMax = 0;
+ this.currentRatio = 0;
+
+ this.onScrollCallback = null;
+ this.onScrollCallbackTicking = false;
+ if (this.target) {
+ // console.log("addEventListner scroll");
+ this.target.parentElement.addEventListener('scroll', () => {
+ if (this.onScrollCallbackTicking) {
+ return;
+ }
+ window.requestAnimationFrame(() => {
+ if (this.onScrollCallback) {
+ this.onScrollCallback();
+ }
+ this.onScrollCallbackTicking = false;
+ });
+ this.onScrollCallbackTicking = true;
+ });
+ }
+ }
+
+ /**
+ *
+ * @param {number} ratio how long against to the viewport height (1.0 to exact same as viewport's height)
+ */
+ setScrollbarSize(ratio) {
+ // if (isNaN(ratio)) {
+ // console.warn(`setScrollbarSize(): ratio is Nan = ${ratio}`);
+ // }
+ // if (ratio < 0 || ratio > 1) {
+ // console.warn(`setScrollbarSize(): ratio is out of range 0-1 but ${ratio}`);
+ // }
+ // console.log(`ScrollbarHelper.setScrollbarSize(): ratio=${ratio}`);
+ this.target.style.height = `${100 * ratio}%`;
+ }
+
+ getScrollbarPosition() {
+ const scrollbarElem = this.target.parentElement;
+ const scrollTopMin = 0;
+ const scrollTopMax = scrollbarElem.scrollHeight - scrollbarElem.clientHeight;
+ const ratioOnScrollbar = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin);
+ this.currentRatio = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin);
+ let pos = this.currentRatio * (this.rangeMax - this.pageSize - this.rangeMin) + this.rangeMin;
+ // console.log(`ScrollbarHelper.getScrollbarPosition(): ratio=${ratioOnScrollbar}, pos=${pos}, scrollTop=${scrollbarElem.scrollTop}, scrollTopMin=${scrollTopMin}, scrollTopMax=${scrollTopMax}, rangeMin=${this.rangeMin}, rangeMax=${this.rangeMax}, pageSize=${this.pageSize}`);
+ if (isNaN(pos)) {
+ return 0;
+ } else {
+ return pos;
+ }
+ }
+
+ /**
+ * @param {number} value in range of rangeMin and rangeMax
+ */
+ setScrollbarPosition(position) {
+ const positionMin = this.rangeMin;
+ const positionMax = this.rangeMax - this.pageSize;
+ const currentPositionRatio = (position - positionMin) / (positionMax - positionMin);
+
+ const scrollbarElement = this.target.parentElement;
+ const scrollTopMin = 0;
+ const scrollTopMax = scrollbarElement.scrollHeight - scrollbarElement.clientHeight;
+ const scrollWidth = scrollTopMax - scrollTopMin;
+ const newScrollTop = currentPositionRatio * scrollWidth;
+ scrollbarElement.scrollTop = currentPositionRatio * scrollWidth;
+
+ // console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, currentPositionRatio=${currentPositionRatio}, calculated scrollTop=${newScrollTop}`);
+
+ if (false) {
+ // const ratio = (position - this.rangeMin) * ((this.rangeMax - this.pageSize) - this.rangeMin);
+
+ const scrollTopMin = 0;
+ const scrollTopMax = this.target.scrollHeight - this.target.clientHeight;
+ const scrollWidth = scrollTopMax - scrollTopMin;
+ const newScrollTop = ratio * scrollWidth;
+ // this.target.parentElement.scrollTop = ratio * scrollWidth;
+ this.target.scrollTop = ratio * scrollWidth;
+
+ console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, ratio=${ratio}, calculated scrollTop=${newScrollTop}`);
+ }
+ }
+
+ /** */
+ setRange(min, max, pageSize) {
+ this.rangeMin = min;
+ this.rangeMax = max;
+ this.currentRatio = 0;
+
+ this.setPageSize(pageSize);
+ }
+
+ setPageSize(pageSize) {
+ if (this.rangeMin == this.rangeMax) {
+ // console.log(`ScrollbarHelper::setPageSize(): size=${size}, but rangeMin == rangeMax`);
+ return;
+ }
+ this.pageSize = pageSize;
+ const ratio = (this.rangeMax - this.rangeMin) / this.pageSize;
+ // console.log(`ScrollbarHelper::setPageSize(): pageSize=${pageSize}, ratio=${ratio}`);
+ this.setScrollbarSize(ratio);
+ }
+
+ setScrollbarEnabled(enabled) {
+ if (!enabled) {
+ this.target.style.height = "100%";
+ }
+ }
+};
+
+window.ScrollbarHelper = ScrollbarHelper;
diff --git a/src/platform/platform.cpp b/src/platform/platform.cpp
index 1b112217..6df72082 100644
--- a/src/platform/platform.cpp
+++ b/src/platform/platform.cpp
@@ -516,6 +516,12 @@ static Platform::Path ResourcePath(const std::string &name) {
return path;
}
+#elif defined(__EMSCRIPTEN__)
+
+static Platform::Path ResourcePath(const std::string &name) {
+ return Path::From("res/" + name);
+}
+
#elif !defined(WIN32)
# if defined(__linux__)
diff --git a/src/render/gl3shader.h b/src/render/gl3shader.h
index 1831e792..e13929dd 100644
--- a/src/render/gl3shader.h
+++ b/src/render/gl3shader.h
@@ -6,7 +6,7 @@
#ifndef SOLVESPACE_GL3SHADER_H
#define SOLVESPACE_GL3SHADER_H
-#if defined(WIN32)
+#if defined(WIN32) || defined(__EMSCRIPTEN__)
# define GL_APICALL /*static linkage*/
# define GL_GLEXT_PROTOTYPES
# include
diff --git a/src/solvespace.cpp b/src/solvespace.cpp
index a8d1a151..b0e48016 100644
--- a/src/solvespace.cpp
+++ b/src/solvespace.cpp
@@ -125,12 +125,8 @@ void SolveSpaceUI::Init() {
SetLocale(locale);
}
- generateAllTimer = Platform::CreateTimer();
- generateAllTimer->onTimeout = std::bind(&SolveSpaceUI::GenerateAll, &SS, Generate::DIRTY,
- /*andFindFree=*/false, /*genForBBox=*/false);
-
- showTWTimer = Platform::CreateTimer();
- showTWTimer->onTimeout = std::bind(&TextWindow::Show, &TW);
+ refreshTimer = Platform::CreateTimer();
+ refreshTimer->onTimeout = std::bind(&SolveSpaceUI::Refresh, &SS);
autosaveTimer = Platform::CreateTimer();
autosaveTimer->onTimeout = std::bind(&SolveSpaceUI::Autosave, &SS);
@@ -302,12 +298,26 @@ void SolveSpaceUI::Exit() {
Platform::ExitGui();
}
+void SolveSpaceUI::Refresh() {
+ // generateAll must happen bfore updating displays
+ if(scheduledGenerateAll) {
+ GenerateAll(Generate::DIRTY, /*andFindFree=*/false, /*genForBBox=*/false);
+ scheduledGenerateAll = false;
+ }
+ if(scheduledShowTW) {
+ TW.Show();
+ scheduledShowTW = false;
+ }
+}
+
void SolveSpaceUI::ScheduleGenerateAll() {
- generateAllTimer->RunAfterProcessingEvents();
+ scheduledGenerateAll = true;
+ refreshTimer->RunAfterProcessingEvents();
}
void SolveSpaceUI::ScheduleShowTW() {
- showTWTimer->RunAfterProcessingEvents();
+ scheduledShowTW = true;
+ refreshTimer->RunAfterProcessingEvents();
}
void SolveSpaceUI::ScheduleAutosave() {
@@ -550,12 +560,18 @@ bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
if(saveAs || saveFile.IsEmpty()) {
Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(GW.window);
+ // FIXME(emscripten):
+ dbp("Calling AddFilter()...");
dialog->AddFilter(C_("file-type", "SolveSpace models"), { SKETCH_EXT });
+ dbp("Calling ThawChoices()...");
dialog->ThawChoices(settings, "Sketch");
if(!newSaveFile.IsEmpty()) {
+ dbp("Calling SetFilename()...");
dialog->SetFilename(newSaveFile);
}
+ dbp("Calling RunModal()...");
if(dialog->RunModal()) {
+ dbp("Calling FreezeChoices()...");
dialog->FreezeChoices(settings, "Sketch");
newSaveFile = dialog->GetFilename();
} else {
@@ -568,6 +584,9 @@ bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
RemoveAutosave();
saveFile = newSaveFile;
unsaved = false;
+ if (this->OnSaveFinished) {
+ this->OnSaveFinished(newSaveFile, saveAs, false);
+ }
return true;
} else {
return false;
@@ -579,7 +598,11 @@ void SolveSpaceUI::Autosave()
ScheduleAutosave();
if(!saveFile.IsEmpty() && unsaved) {
- SaveToFile(saveFile.WithExtension(BACKUP_EXT));
+ Platform::Path saveFileName = saveFile.WithExtension(BACKUP_EXT);
+ SaveToFile(saveFileName);
+ if (this->OnSaveFinished) {
+ this->OnSaveFinished(saveFileName, false, true);
+ }
}
}
@@ -679,6 +702,9 @@ void SolveSpaceUI::MenuFile(Command id) {
if(dialog->RunModal()) {
dialog->FreezeChoices(settings, "ExportImage");
SS.ExportAsPngTo(dialog->GetFilename());
+ if (SS.OnSaveFinished) {
+ SS.OnSaveFinished(dialog->GetFilename(), false, false);
+ }
}
break;
}
@@ -704,6 +730,9 @@ void SolveSpaceUI::MenuFile(Command id) {
}
SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false);
+ if (SS.OnSaveFinished) {
+ SS.OnSaveFinished(dialog->GetFilename(), false, false);
+ }
break;
}
@@ -716,6 +745,9 @@ void SolveSpaceUI::MenuFile(Command id) {
dialog->FreezeChoices(settings, "ExportWireframe");
SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true);
+ if (SS.OnSaveFinished) {
+ SS.OnSaveFinished(dialog->GetFilename(), false, false);
+ }
break;
}
@@ -728,6 +760,9 @@ void SolveSpaceUI::MenuFile(Command id) {
dialog->FreezeChoices(settings, "ExportSection");
SS.ExportSectionTo(dialog->GetFilename());
+ if (SS.OnSaveFinished) {
+ SS.OnSaveFinished(dialog->GetFilename(), false, false);
+ }
break;
}
@@ -740,6 +775,10 @@ void SolveSpaceUI::MenuFile(Command id) {
dialog->FreezeChoices(settings, "ExportMesh");
SS.ExportMeshTo(dialog->GetFilename());
+ if (SS.OnSaveFinished) {
+ SS.OnSaveFinished(dialog->GetFilename(), false, false);
+ }
+
break;
}
@@ -753,6 +792,9 @@ void SolveSpaceUI::MenuFile(Command id) {
StepFileWriter sfw = {};
sfw.ExportSurfacesTo(dialog->GetFilename());
+ if (SS.OnSaveFinished) {
+ SS.OnSaveFinished(dialog->GetFilename(), false, false);
+ }
break;
}
diff --git a/src/solvespace.h b/src/solvespace.h
index 1c22db62..2568a70b 100644
--- a/src/solvespace.h
+++ b/src/solvespace.h
@@ -683,6 +683,7 @@ public:
void NewFile();
bool SaveToFile(const Platform::Path &filename);
bool LoadAutosaveFor(const Platform::Path &filename);
+ std::function OnSaveFinished;
bool LoadFromFile(const Platform::Path &filename, bool canCancel = false);
void UpgradeLegacyData();
bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le,
@@ -793,9 +794,11 @@ public:
// the sketch!
bool allConsistent;
- Platform::TimerRef showTWTimer;
- Platform::TimerRef generateAllTimer;
+ bool scheduledGenerateAll;
+ bool scheduledShowTW;
+ Platform::TimerRef refreshTimer;
Platform::TimerRef autosaveTimer;
+ void Refresh();
void ScheduleShowTW();
void ScheduleGenerateAll();
void ScheduleAutosave();
diff --git a/src/srf/shell.cpp b/src/srf/shell.cpp
new file mode 100644
index 00000000..ca3b75ee
--- /dev/null
+++ b/src/srf/shell.cpp
@@ -0,0 +1,614 @@
+//-----------------------------------------------------------------------------
+// Anything involving NURBS shells (i.e., shells); except
+// for the real math, which is in ratpoly.cpp.
+//
+// Copyright 2008-2013 Jonathan Westhues.
+//-----------------------------------------------------------------------------
+#include "../solvespace.h"
+
+typedef struct {
+ hSCurve hc;
+ hSSurface hs;
+} TrimLine;
+
+
+void SShell::MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1, RgbaColor color)
+{
+ // Make the extrusion direction consistent with respect to the normal
+ // of the sketch we're extruding.
+ if((t0.Minus(t1)).Dot(sbls->normal) < 0) {
+ swap(t0, t1);
+ }
+
+ // Define a coordinate system to contain the original sketch, and get
+ // a bounding box in that csys
+ Vector n = sbls->normal.ScaledBy(-1);
+ Vector u = n.Normal(0), v = n.Normal(1);
+ Vector orig = sbls->point;
+ double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
+ sbls->GetBoundingProjd(u, orig, &umin, &umax);
+ double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
+ sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
+ // and now fix things up so that all u and v lie between 0 and 1
+ orig = orig.Plus(u.ScaledBy(umin));
+ orig = orig.Plus(v.ScaledBy(vmin));
+ u = u.ScaledBy(umax - umin);
+ v = v.ScaledBy(vmax - vmin);
+
+ // So we can now generate the top and bottom surfaces of the extrusion,
+ // planes within a translated (and maybe mirrored) version of that csys.
+ SSurface s0, s1;
+ s0 = SSurface::FromPlane(orig.Plus(t0), u, v);
+ s0.color = color;
+ s1 = SSurface::FromPlane(orig.Plus(t1).Plus(u), u.ScaledBy(-1), v);
+ s1.color = color;
+ hSSurface hs0 = surface.AddAndAssignId(&s0),
+ hs1 = surface.AddAndAssignId(&s1);
+
+ // Now go through the input curves. For each one, generate its surface
+ // of extrusion, its two translated trim curves, and one trim line. We
+ // go through by loops so that we can assign the lines correctly.
+ SBezierLoop *sbl;
+ for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
+ SBezier *sb;
+ List trimLines = {};
+
+ for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
+ // Generate the surface of extrusion of this curve, and add
+ // it to the list
+ SSurface ss = SSurface::FromExtrusionOf(sb, t0, t1);
+ ss.color = color;
+ hSSurface hsext = surface.AddAndAssignId(&ss);
+
+ // Translate the curve by t0 and t1 to produce two trim curves
+ SCurve sc = {};
+ sc.isExact = true;
+ sc.exact = sb->TransformedBy(t0, Quaternion::IDENTITY, 1.0);
+ (sc.exact).MakePwlInto(&(sc.pts));
+ sc.surfA = hs0;
+ sc.surfB = hsext;
+ hSCurve hc0 = curve.AddAndAssignId(&sc);
+
+ sc = {};
+ sc.isExact = true;
+ sc.exact = sb->TransformedBy(t1, Quaternion::IDENTITY, 1.0);
+ (sc.exact).MakePwlInto(&(sc.pts));
+ sc.surfA = hs1;
+ sc.surfB = hsext;
+ hSCurve hc1 = curve.AddAndAssignId(&sc);
+
+ STrimBy stb0, stb1;
+ // The translated curves trim the flat top and bottom surfaces.
+ stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/false);
+ stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/true);
+ (surface.FindById(hs0))->trim.Add(&stb0);
+ (surface.FindById(hs1))->trim.Add(&stb1);
+
+ // The translated curves also trim the surface of extrusion.
+ stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/true);
+ stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/false);
+ (surface.FindById(hsext))->trim.Add(&stb0);
+ (surface.FindById(hsext))->trim.Add(&stb1);
+
+ // And form the trim line
+ Vector pt = sb->Finish();
+ sc = {};
+ sc.isExact = true;
+ sc.exact = SBezier::From(pt.Plus(t0), pt.Plus(t1));
+ (sc.exact).MakePwlInto(&(sc.pts));
+ hSCurve hl = curve.AddAndAssignId(&sc);
+ // save this for later
+ TrimLine tl;
+ tl.hc = hl;
+ tl.hs = hsext;
+ trimLines.Add(&tl);
+ }
+
+ int i;
+ for(i = 0; i < trimLines.n; i++) {
+ TrimLine *tl = &(trimLines[i]);
+ SSurface *ss = surface.FindById(tl->hs);
+
+ TrimLine *tlp = &(trimLines[WRAP(i-1, trimLines.n)]);
+
+ STrimBy stb;
+ stb = STrimBy::EntireCurve(this, tl->hc, /*backwards=*/true);
+ ss->trim.Add(&stb);
+ stb = STrimBy::EntireCurve(this, tlp->hc, /*backwards=*/false);
+ ss->trim.Add(&stb);
+
+ (curve.FindById(tl->hc))->surfA = ss->h;
+ (curve.FindById(tlp->hc))->surfB = ss->h;
+ }
+ trimLines.Clear();
+ }
+}
+
+bool SShell::CheckNormalAxisRelationship(SBezierLoopSet *sbls, Vector pt, Vector axis, double da, double dx)
+// Check that the direction of revolution/extrusion ends up parallel to the normal of
+// the sketch, on the side of the axis where the sketch is.
+{
+ SBezierLoop *sbl;
+ Vector pto;
+ double md = VERY_NEGATIVE;
+ for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
+ SBezier *sb;
+ for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
+ // Choose the point farthest from the axis; we'll get garbage
+ // if we choose a point that lies on the axis, for example.
+ // (And our surface will be self-intersecting if the sketch
+ // spans the axis, so don't worry about that.)
+ for(int i = 0; i <= sb->deg; i++) {
+ Vector p = sb->ctrl[i];
+ double d = p.DistanceToLine(pt, axis);
+ if(d > md) {
+ md = d;
+ pto = p;
+ }
+ }
+ }
+ }
+ Vector ptc = pto.ClosestPointOnLine(pt, axis),
+ up = axis.Cross(pto.Minus(ptc)).ScaledBy(da),
+ vp = up.Plus(axis.ScaledBy(dx));
+
+ return (vp.Dot(sbls->normal) > 0);
+}
+
+// sketch must not contain the axis of revolution as a non-construction line for helix
+void SShell::MakeFromHelicalRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis,
+ RgbaColor color, Group *group, double angles,
+ double anglef, double dists, double distf) {
+ int i0 = surface.n; // number of pre-existing surfaces
+ SBezierLoop *sbl;
+ // for testing - hard code the axial distance, and number of sections.
+ // distance will need to be parameters in the future.
+ double dist = distf - dists;
+ int sections = (int)(fabs(anglef - angles) / (PI / 2) + 1);
+ double wedge = (anglef - angles) / sections;
+ int startMapping = Group::REMAP_LATHE_START, endMapping = Group::REMAP_LATHE_END;
+
+ if(CheckNormalAxisRelationship(sbls, pt, axis, anglef-angles, distf-dists)) {
+ swap(angles, anglef);
+ swap(dists, distf);
+ dist = -dist;
+ wedge = -wedge;
+ swap(startMapping, endMapping);
+ }
+
+ // Define a coordinate system to contain the original sketch, and get
+ // a bounding box in that csys
+ Vector n = sbls->normal.ScaledBy(-1);
+ Vector u = n.Normal(0), v = n.Normal(1);
+ Vector orig = sbls->point;
+ double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
+ sbls->GetBoundingProjd(u, orig, &umin, &umax);
+ double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
+ sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
+ // and now fix things up so that all u and v lie between 0 and 1
+ orig = orig.Plus(u.ScaledBy(umin));
+ orig = orig.Plus(v.ScaledBy(vmin));
+ u = u.ScaledBy(umax - umin);
+ v = v.ScaledBy(vmax - vmin);
+
+ // So we can now generate the end caps of the extrusion within
+ // a translated and rotated (and maybe mirrored) version of that csys.
+ SSurface s0, s1;
+ s0 = SSurface::FromPlane(orig.RotatedAbout(pt, axis, angles).Plus(axis.ScaledBy(dists)),
+ u.RotatedAbout(axis, angles), v.RotatedAbout(axis, angles));
+ s0.color = color;
+
+ hEntity face0 = group->Remap(Entity::NO_ENTITY, startMapping);
+ s0.face = face0.v;
+
+ s1 = SSurface::FromPlane(
+ orig.Plus(u).RotatedAbout(pt, axis, anglef).Plus(axis.ScaledBy(distf)),
+ u.ScaledBy(-1).RotatedAbout(axis, anglef), v.RotatedAbout(axis, anglef));
+ s1.color = color;
+
+ hEntity face1 = group->Remap(Entity::NO_ENTITY, endMapping);
+ s1.face = face1.v;
+
+ hSSurface hs0 = surface.AddAndAssignId(&s0);
+ hSSurface hs1 = surface.AddAndAssignId(&s1);
+
+ // Now we actually build and trim the swept surfaces. One loop at a time.
+ for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
+ int i, j;
+ SBezier *sb;
+ List> hsl = {};
+
+ // This is where all the NURBS are created and Remapped to the generating curve
+ for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
+ std::vector revs(sections);
+ for(j = 0; j < sections; j++) {
+ if((dist == 0) && sb->deg == 1 &&
+ (sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
+ (sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS) {
+ // This is a line on the axis of revolution; it does
+ // not contribute a surface.
+ revs[j].v = 0;
+ } else {
+ SSurface ss = SSurface::FromRevolutionOf(
+ sb, pt, axis, angles + (wedge)*j, angles + (wedge) * (j + 1),
+ dists + j * dist / sections, dists + (j + 1) * dist / sections);
+ ss.color = color;
+ if(sb->entity != 0) {
+ hEntity he;
+ he.v = sb->entity;
+ hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
+ if(SK.entity.FindByIdNoOops(hface) != NULL) {
+ ss.face = hface.v;
+ }
+ }
+ revs[j] = surface.AddAndAssignId(&ss);
+ }
+ }
+ hsl.Add(&revs);
+ }
+ // Still the same loop. Need to create trim curves
+ for(i = 0; i < sbl->l.n; i++) {
+ std::vector revs = hsl[i], revsp = hsl[WRAP(i - 1, sbl->l.n)];
+
+ sb = &(sbl->l[i]);
+
+ // we will need the grid t-values for this entire row of surfaces
+ List t_values;
+ t_values = {};
+ if (revs[0].v) {
+ double ps = 0.0;
+ t_values.Add(&ps);
+ (surface.FindById(revs[0]))->MakeTriangulationGridInto(
+ &t_values, 0.0, 1.0, true, 0);
+ }
+ // we generate one more curve than we did surfaces
+ for(j = 0; j <= sections; j++) {
+ SCurve sc;
+ Quaternion qs = Quaternion::From(axis, angles + wedge * j);
+ // we want Q*(x - p) + p = Q*x + (p - Q*p)
+ Vector ts =
+ pt.Minus(qs.Rotate(pt)).Plus(axis.ScaledBy(dists + j * dist / sections));
+
+ // If this input curve generated a surface, then trim that
+ // surface with the rotated version of the input curve.
+ if(revs[0].v) { // not d[j] because crash on j==sections
+ sc = {};
+ sc.isExact = true;
+ sc.exact = sb->TransformedBy(ts, qs, 1.0);
+ // make the PWL for the curve based on t value list
+ for(int x = 0; x < t_values.n; x++) {
+ SCurvePt scpt;
+ scpt.tag = 0;
+ scpt.p = sc.exact.PointAt(t_values[x]);
+ scpt.vertex = (x == 0) || (x == (t_values.n - 1));
+ sc.pts.Add(&scpt);
+ }
+
+ // the surfaces already exists so trim with this curve
+ if(j < sections) {
+ sc.surfA = revs[j];
+ } else {
+ sc.surfA = hs1; // end cap
+ }
+
+ if(j > 0) {
+ sc.surfB = revs[j - 1];
+ } else {
+ sc.surfB = hs0; // staring cap
+ }
+
+ hSCurve hcb = curve.AddAndAssignId(&sc);
+
+ STrimBy stb;
+ stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
+ (surface.FindById(sc.surfA))->trim.Add(&stb);
+ stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
+ (surface.FindById(sc.surfB))->trim.Add(&stb);
+ } else if(j == 0) { // curve was on the rotation axis and is shared by the end caps.
+ sc = {};
+ sc.isExact = true;
+ sc.exact = sb->TransformedBy(ts, qs, 1.0);
+ (sc.exact).MakePwlInto(&(sc.pts));
+ sc.surfA = hs1; // end cap
+ sc.surfB = hs0; // staring cap
+ hSCurve hcb = curve.AddAndAssignId(&sc);
+
+ STrimBy stb;
+ stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
+ (surface.FindById(sc.surfA))->trim.Add(&stb);
+ stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
+ (surface.FindById(sc.surfB))->trim.Add(&stb);
+ }
+
+ // And if this input curve and the one after it both generated
+ // surfaces, then trim both of those by the appropriate
+ // curve based on the control points.
+ if((j < sections) && revs[j].v && revsp[j].v) {
+ SSurface *ss = surface.FindById(revs[j]);
+
+ sc = {};
+ sc.isExact = true;
+ sc.exact = SBezier::From(ss->ctrl[0][0], ss->ctrl[0][1], ss->ctrl[0][2]);
+ sc.exact.weight[1] = ss->weight[0][1];
+ double max_dt = 0.5;
+ if (sc.exact.deg > 1) max_dt = 0.125;
+ (sc.exact).MakePwlInto(&(sc.pts), 0.0, max_dt);
+ sc.surfA = revs[j];
+ sc.surfB = revsp[j];
+
+ hSCurve hcc = curve.AddAndAssignId(&sc);
+
+ STrimBy stb;
+ stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
+ (surface.FindById(sc.surfA))->trim.Add(&stb);
+ stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
+ (surface.FindById(sc.surfB))->trim.Add(&stb);
+ }
+ }
+ t_values.Clear();
+ }
+
+ hsl.Clear();
+ }
+
+ if(dist == 0) {
+ MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
+ }
+}
+
+void SShell::MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, RgbaColor color,
+ Group *group) {
+ int i0 = surface.n; // number of pre-existing surfaces
+ SBezierLoop *sbl;
+
+ if(CheckNormalAxisRelationship(sbls, pt, axis, 1.0, 0.0)) {
+ axis = axis.ScaledBy(-1);
+ }
+
+ // Now we actually build and trim the surfaces.
+ for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
+ int i, j;
+ SBezier *sb;
+ List> hsl = {};
+
+ for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
+ std::vector revs(4);
+ for(j = 0; j < 4; j++) {
+ if(sb->deg == 1 &&
+ (sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
+ (sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS)
+ {
+ // This is a line on the axis of revolution; it does
+ // not contribute a surface.
+ revs[j].v = 0;
+ } else {
+ SSurface ss = SSurface::FromRevolutionOf(sb, pt, axis, (PI / 2) * j,
+ (PI / 2) * (j + 1), 0.0, 0.0);
+ ss.color = color;
+ if(sb->entity != 0) {
+ hEntity he;
+ he.v = sb->entity;
+ hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
+ if(SK.entity.FindByIdNoOops(hface) != NULL) {
+ ss.face = hface.v;
+ }
+ }
+ revs[j] = surface.AddAndAssignId(&ss);
+ }
+ }
+ hsl.Add(&revs);
+ }
+
+ for(i = 0; i < sbl->l.n; i++) {
+ std::vector revs = hsl[i],
+ revsp = hsl[WRAP(i-1, sbl->l.n)];
+
+ sb = &(sbl->l[i]);
+
+ for(j = 0; j < 4; j++) {
+ SCurve sc;
+ Quaternion qs = Quaternion::From(axis, (PI/2)*j);
+ // we want Q*(x - p) + p = Q*x + (p - Q*p)
+ Vector ts = pt.Minus(qs.Rotate(pt));
+
+ // If this input curve generate a surface, then trim that
+ // surface with the rotated version of the input curve.
+ if(revs[j].v) {
+ sc = {};
+ sc.isExact = true;
+ sc.exact = sb->TransformedBy(ts, qs, 1.0);
+ (sc.exact).MakePwlInto(&(sc.pts));
+ sc.surfA = revs[j];
+ sc.surfB = revs[WRAP(j-1, 4)];
+
+ hSCurve hcb = curve.AddAndAssignId(&sc);
+
+ STrimBy stb;
+ stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
+ (surface.FindById(sc.surfA))->trim.Add(&stb);
+ stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
+ (surface.FindById(sc.surfB))->trim.Add(&stb);
+ }
+
+ // And if this input curve and the one after it both generated
+ // surfaces, then trim both of those by the appropriate
+ // circle.
+ if(revs[j].v && revsp[j].v) {
+ SSurface *ss = surface.FindById(revs[j]);
+
+ sc = {};
+ sc.isExact = true;
+ sc.exact = SBezier::From(ss->ctrl[0][0],
+ ss->ctrl[0][1],
+ ss->ctrl[0][2]);
+ sc.exact.weight[1] = ss->weight[0][1];
+ (sc.exact).MakePwlInto(&(sc.pts));
+ sc.surfA = revs[j];
+ sc.surfB = revsp[j];
+
+ hSCurve hcc = curve.AddAndAssignId(&sc);
+
+ STrimBy stb;
+ stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
+ (surface.FindById(sc.surfA))->trim.Add(&stb);
+ stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
+ (surface.FindById(sc.surfB))->trim.Add(&stb);
+ }
+ }
+ }
+
+ hsl.Clear();
+ }
+
+ MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
+}
+
+void SShell::MakeFirstOrderRevolvedSurfaces(Vector pt, Vector axis, int i0) {
+ int i;
+
+ for(i = i0; i < surface.n; i++) {
+ SSurface *srf = &(surface[i]);
+
+ // Revolution of a line; this is potentially a plane, which we can
+ // rewrite to have degree (1, 1).
+ if(srf->degm == 1 && srf->degn == 2) {
+ // close start, far start, far finish
+ Vector cs, fs, ff;
+ double d0, d1;
+ d0 = (srf->ctrl[0][0]).DistanceToLine(pt, axis);
+ d1 = (srf->ctrl[1][0]).DistanceToLine(pt, axis);
+
+ if(d0 > d1) {
+ cs = srf->ctrl[1][0];
+ fs = srf->ctrl[0][0];
+ ff = srf->ctrl[0][2];
+ } else {
+ cs = srf->ctrl[0][0];
+ fs = srf->ctrl[1][0];
+ ff = srf->ctrl[1][2];
+ }
+
+ // origin close, origin far
+ Vector oc = cs.ClosestPointOnLine(pt, axis),
+ of = fs.ClosestPointOnLine(pt, axis);
+
+ if(oc.Equals(of)) {
+ // This is a plane, not a (non-degenerate) cone.
+ Vector oldn = srf->NormalAt(0.5, 0.5);
+
+ Vector u = fs.Minus(of), v;
+
+ v = (axis.Cross(u)).WithMagnitude(1);
+
+ double vm = (ff.Minus(of)).Dot(v);
+ v = v.ScaledBy(vm);
+
+ srf->degm = 1;
+ srf->degn = 1;
+ srf->ctrl[0][0] = of;
+ srf->ctrl[0][1] = of.Plus(u);
+ srf->ctrl[1][0] = of.Plus(v);
+ srf->ctrl[1][1] = of.Plus(u).Plus(v);
+ srf->weight[0][0] = 1;
+ srf->weight[0][1] = 1;
+ srf->weight[1][0] = 1;
+ srf->weight[1][1] = 1;
+
+ if(oldn.Dot(srf->NormalAt(0.5, 0.5)) < 0) {
+ swap(srf->ctrl[0][0], srf->ctrl[1][0]);
+ swap(srf->ctrl[0][1], srf->ctrl[1][1]);
+ }
+ continue;
+ }
+
+ if(fabs(d0 - d1) < LENGTH_EPS) {
+ // This is a cylinder; so transpose it so that we'll recognize
+ // it as a surface of extrusion.
+ SSurface sn = *srf;
+
+ // Transposing u and v flips the normal, so reverse u to
+ // flip it again and put it back where we started.
+ sn.degm = 2;
+ sn.degn = 1;
+ int dm, dn;
+ for(dm = 0; dm <= 1; dm++) {
+ for(dn = 0; dn <= 2; dn++) {
+ sn.ctrl [dn][dm] = srf->ctrl [1-dm][dn];
+ sn.weight[dn][dm] = srf->weight[1-dm][dn];
+ }
+ }
+
+ *srf = sn;
+ continue;
+ }
+ }
+ }
+}
+
+void SShell::MakeFromCopyOf(SShell *a) {
+ ssassert(this != a, "Can't make from copy of self");
+ MakeFromTransformationOf(a,
+ Vector::From(0, 0, 0), Quaternion::IDENTITY, 1.0);
+}
+
+void SShell::MakeFromTransformationOf(SShell *a,
+ Vector t, Quaternion q, double scale)
+{
+ booleanFailed = false;
+ surface.ReserveMore(a->surface.n);
+ for(SSurface &s : a->surface) {
+ SSurface n;
+ n = SSurface::FromTransformationOf(&s, t, q, scale, /*includingTrims=*/true);
+ surface.Add(&n); // keeping the old ID
+ }
+
+ curve.ReserveMore(a->curve.n);
+ for(SCurve &c : a->curve) {
+ SCurve n;
+ n = SCurve::FromTransformationOf(&c, t, q, scale);
+ curve.Add(&n); // keeping the old ID
+ }
+}
+
+void SShell::MakeEdgesInto(SEdgeList *sel) {
+ for(SSurface &s : surface) {
+ s.MakeEdgesInto(this, sel, SSurface::MakeAs::XYZ);
+ }
+}
+
+void SShell::MakeSectionEdgesInto(Vector n, double d, SEdgeList *sel, SBezierList *sbl)
+{
+ for(SSurface &s : surface) {
+ if(s.CoincidentWithPlane(n, d)) {
+ s.MakeSectionEdgesInto(this, sel, sbl);
+ }
+ }
+}
+
+void SShell::TriangulateInto(SMesh *sm) {
+#pragma omp parallel for
+ for(int i=0; iTriangulateInto(this, &m);
+ #pragma omp critical
+ sm->MakeFromCopyOf(&m);
+ m.Clear();
+ }
+}
+
+bool SShell::IsEmpty() const {
+ return surface.IsEmpty();
+}
+
+void SShell::Clear() {
+ for(SSurface &s : surface) {
+ s.Clear();
+ }
+ surface.Clear();
+
+ for(SCurve &c : curve) {
+ c.Clear();
+ }
+ curve.Clear();
+}
diff --git a/src/srf/surface.cpp b/src/srf/surface.cpp
index c63875e5..18edabf6 100644
--- a/src/srf/surface.cpp
+++ b/src/srf/surface.cpp
@@ -489,608 +489,4 @@ void SSurface::Clear() {
trim.Clear();
}
-typedef struct {
- hSCurve hc;
- hSSurface hs;
-} TrimLine;
-void SShell::MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1, RgbaColor color)
-{
- // Make the extrusion direction consistent with respect to the normal
- // of the sketch we're extruding.
- if((t0.Minus(t1)).Dot(sbls->normal) < 0) {
- swap(t0, t1);
- }
-
- // Define a coordinate system to contain the original sketch, and get
- // a bounding box in that csys
- Vector n = sbls->normal.ScaledBy(-1);
- Vector u = n.Normal(0), v = n.Normal(1);
- Vector orig = sbls->point;
- double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
- sbls->GetBoundingProjd(u, orig, &umin, &umax);
- double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
- sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
- // and now fix things up so that all u and v lie between 0 and 1
- orig = orig.Plus(u.ScaledBy(umin));
- orig = orig.Plus(v.ScaledBy(vmin));
- u = u.ScaledBy(umax - umin);
- v = v.ScaledBy(vmax - vmin);
-
- // So we can now generate the top and bottom surfaces of the extrusion,
- // planes within a translated (and maybe mirrored) version of that csys.
- SSurface s0, s1;
- s0 = SSurface::FromPlane(orig.Plus(t0), u, v);
- s0.color = color;
- s1 = SSurface::FromPlane(orig.Plus(t1).Plus(u), u.ScaledBy(-1), v);
- s1.color = color;
- hSSurface hs0 = surface.AddAndAssignId(&s0),
- hs1 = surface.AddAndAssignId(&s1);
-
- // Now go through the input curves. For each one, generate its surface
- // of extrusion, its two translated trim curves, and one trim line. We
- // go through by loops so that we can assign the lines correctly.
- SBezierLoop *sbl;
- for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
- SBezier *sb;
- List trimLines = {};
-
- for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
- // Generate the surface of extrusion of this curve, and add
- // it to the list
- SSurface ss = SSurface::FromExtrusionOf(sb, t0, t1);
- ss.color = color;
- hSSurface hsext = surface.AddAndAssignId(&ss);
-
- // Translate the curve by t0 and t1 to produce two trim curves
- SCurve sc = {};
- sc.isExact = true;
- sc.exact = sb->TransformedBy(t0, Quaternion::IDENTITY, 1.0);
- (sc.exact).MakePwlInto(&(sc.pts));
- sc.surfA = hs0;
- sc.surfB = hsext;
- hSCurve hc0 = curve.AddAndAssignId(&sc);
-
- sc = {};
- sc.isExact = true;
- sc.exact = sb->TransformedBy(t1, Quaternion::IDENTITY, 1.0);
- (sc.exact).MakePwlInto(&(sc.pts));
- sc.surfA = hs1;
- sc.surfB = hsext;
- hSCurve hc1 = curve.AddAndAssignId(&sc);
-
- STrimBy stb0, stb1;
- // The translated curves trim the flat top and bottom surfaces.
- stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/false);
- stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/true);
- (surface.FindById(hs0))->trim.Add(&stb0);
- (surface.FindById(hs1))->trim.Add(&stb1);
-
- // The translated curves also trim the surface of extrusion.
- stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/true);
- stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/false);
- (surface.FindById(hsext))->trim.Add(&stb0);
- (surface.FindById(hsext))->trim.Add(&stb1);
-
- // And form the trim line
- Vector pt = sb->Finish();
- sc = {};
- sc.isExact = true;
- sc.exact = SBezier::From(pt.Plus(t0), pt.Plus(t1));
- (sc.exact).MakePwlInto(&(sc.pts));
- hSCurve hl = curve.AddAndAssignId(&sc);
- // save this for later
- TrimLine tl;
- tl.hc = hl;
- tl.hs = hsext;
- trimLines.Add(&tl);
- }
-
- int i;
- for(i = 0; i < trimLines.n; i++) {
- TrimLine *tl = &(trimLines[i]);
- SSurface *ss = surface.FindById(tl->hs);
-
- TrimLine *tlp = &(trimLines[WRAP(i-1, trimLines.n)]);
-
- STrimBy stb;
- stb = STrimBy::EntireCurve(this, tl->hc, /*backwards=*/true);
- ss->trim.Add(&stb);
- stb = STrimBy::EntireCurve(this, tlp->hc, /*backwards=*/false);
- ss->trim.Add(&stb);
-
- (curve.FindById(tl->hc))->surfA = ss->h;
- (curve.FindById(tlp->hc))->surfB = ss->h;
- }
- trimLines.Clear();
- }
-}
-
-bool SShell::CheckNormalAxisRelationship(SBezierLoopSet *sbls, Vector pt, Vector axis, double da, double dx)
-// Check that the direction of revolution/extrusion ends up parallel to the normal of
-// the sketch, on the side of the axis where the sketch is.
-{
- SBezierLoop *sbl;
- Vector pto;
- double md = VERY_NEGATIVE;
- for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
- SBezier *sb;
- for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
- // Choose the point farthest from the axis; we'll get garbage
- // if we choose a point that lies on the axis, for example.
- // (And our surface will be self-intersecting if the sketch
- // spans the axis, so don't worry about that.)
- for(int i = 0; i <= sb->deg; i++) {
- Vector p = sb->ctrl[i];
- double d = p.DistanceToLine(pt, axis);
- if(d > md) {
- md = d;
- pto = p;
- }
- }
- }
- }
- Vector ptc = pto.ClosestPointOnLine(pt, axis),
- up = axis.Cross(pto.Minus(ptc)).ScaledBy(da),
- vp = up.Plus(axis.ScaledBy(dx));
-
- return (vp.Dot(sbls->normal) > 0);
-}
-
-// sketch must not contain the axis of revolution as a non-construction line for helix
-void SShell::MakeFromHelicalRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis,
- RgbaColor color, Group *group, double angles,
- double anglef, double dists, double distf) {
- int i0 = surface.n; // number of pre-existing surfaces
- SBezierLoop *sbl;
- // for testing - hard code the axial distance, and number of sections.
- // distance will need to be parameters in the future.
- double dist = distf - dists;
- int sections = (int)(fabs(anglef - angles) / (PI / 2) + 1);
- double wedge = (anglef - angles) / sections;
- int startMapping = Group::REMAP_LATHE_START, endMapping = Group::REMAP_LATHE_END;
-
- if(CheckNormalAxisRelationship(sbls, pt, axis, anglef-angles, distf-dists)) {
- swap(angles, anglef);
- swap(dists, distf);
- dist = -dist;
- wedge = -wedge;
- swap(startMapping, endMapping);
- }
-
- // Define a coordinate system to contain the original sketch, and get
- // a bounding box in that csys
- Vector n = sbls->normal.ScaledBy(-1);
- Vector u = n.Normal(0), v = n.Normal(1);
- Vector orig = sbls->point;
- double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
- sbls->GetBoundingProjd(u, orig, &umin, &umax);
- double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
- sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
- // and now fix things up so that all u and v lie between 0 and 1
- orig = orig.Plus(u.ScaledBy(umin));
- orig = orig.Plus(v.ScaledBy(vmin));
- u = u.ScaledBy(umax - umin);
- v = v.ScaledBy(vmax - vmin);
-
- // So we can now generate the end caps of the extrusion within
- // a translated and rotated (and maybe mirrored) version of that csys.
- SSurface s0, s1;
- s0 = SSurface::FromPlane(orig.RotatedAbout(pt, axis, angles).Plus(axis.ScaledBy(dists)),
- u.RotatedAbout(axis, angles), v.RotatedAbout(axis, angles));
- s0.color = color;
-
- hEntity face0 = group->Remap(Entity::NO_ENTITY, startMapping);
- s0.face = face0.v;
-
- s1 = SSurface::FromPlane(
- orig.Plus(u).RotatedAbout(pt, axis, anglef).Plus(axis.ScaledBy(distf)),
- u.ScaledBy(-1).RotatedAbout(axis, anglef), v.RotatedAbout(axis, anglef));
- s1.color = color;
-
- hEntity face1 = group->Remap(Entity::NO_ENTITY, endMapping);
- s1.face = face1.v;
-
- hSSurface hs0 = surface.AddAndAssignId(&s0);
- hSSurface hs1 = surface.AddAndAssignId(&s1);
-
- // Now we actually build and trim the swept surfaces. One loop at a time.
- for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
- int i, j;
- SBezier *sb;
- List> hsl = {};
-
- // This is where all the NURBS are created and Remapped to the generating curve
- for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
- std::vector revs(sections);
- for(j = 0; j < sections; j++) {
- if((dist == 0) && sb->deg == 1 &&
- (sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
- (sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS) {
- // This is a line on the axis of revolution; it does
- // not contribute a surface.
- revs[j].v = 0;
- } else {
- SSurface ss = SSurface::FromRevolutionOf(
- sb, pt, axis, angles + (wedge)*j, angles + (wedge) * (j + 1),
- dists + j * dist / sections, dists + (j + 1) * dist / sections);
- ss.color = color;
- if(sb->entity != 0) {
- hEntity he;
- he.v = sb->entity;
- hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
- if(SK.entity.FindByIdNoOops(hface) != NULL) {
- ss.face = hface.v;
- }
- }
- revs[j] = surface.AddAndAssignId(&ss);
- }
- }
- hsl.Add(&revs);
- }
- // Still the same loop. Need to create trim curves
- for(i = 0; i < sbl->l.n; i++) {
- std::vector revs = hsl[i], revsp = hsl[WRAP(i - 1, sbl->l.n)];
-
- sb = &(sbl->l[i]);
-
- // we will need the grid t-values for this entire row of surfaces
- List t_values;
- t_values = {};
- if (revs[0].v) {
- double ps = 0.0;
- t_values.Add(&ps);
- (surface.FindById(revs[0]))->MakeTriangulationGridInto(
- &t_values, 0.0, 1.0, true, 0);
- }
- // we generate one more curve than we did surfaces
- for(j = 0; j <= sections; j++) {
- SCurve sc;
- Quaternion qs = Quaternion::From(axis, angles + wedge * j);
- // we want Q*(x - p) + p = Q*x + (p - Q*p)
- Vector ts =
- pt.Minus(qs.Rotate(pt)).Plus(axis.ScaledBy(dists + j * dist / sections));
-
- // If this input curve generated a surface, then trim that
- // surface with the rotated version of the input curve.
- if(revs[0].v) { // not d[j] because crash on j==sections
- sc = {};
- sc.isExact = true;
- sc.exact = sb->TransformedBy(ts, qs, 1.0);
- // make the PWL for the curve based on t value list
- for(int x = 0; x < t_values.n; x++) {
- SCurvePt scpt;
- scpt.tag = 0;
- scpt.p = sc.exact.PointAt(t_values[x]);
- scpt.vertex = (x == 0) || (x == (t_values.n - 1));
- sc.pts.Add(&scpt);
- }
-
- // the surfaces already exists so trim with this curve
- if(j < sections) {
- sc.surfA = revs[j];
- } else {
- sc.surfA = hs1; // end cap
- }
-
- if(j > 0) {
- sc.surfB = revs[j - 1];
- } else {
- sc.surfB = hs0; // staring cap
- }
-
- hSCurve hcb = curve.AddAndAssignId(&sc);
-
- STrimBy stb;
- stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
- (surface.FindById(sc.surfA))->trim.Add(&stb);
- stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
- (surface.FindById(sc.surfB))->trim.Add(&stb);
- } else if(j == 0) { // curve was on the rotation axis and is shared by the end caps.
- sc = {};
- sc.isExact = true;
- sc.exact = sb->TransformedBy(ts, qs, 1.0);
- (sc.exact).MakePwlInto(&(sc.pts));
- sc.surfA = hs1; // end cap
- sc.surfB = hs0; // staring cap
- hSCurve hcb = curve.AddAndAssignId(&sc);
-
- STrimBy stb;
- stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
- (surface.FindById(sc.surfA))->trim.Add(&stb);
- stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
- (surface.FindById(sc.surfB))->trim.Add(&stb);
- }
-
- // And if this input curve and the one after it both generated
- // surfaces, then trim both of those by the appropriate
- // curve based on the control points.
- if((j < sections) && revs[j].v && revsp[j].v) {
- SSurface *ss = surface.FindById(revs[j]);
-
- sc = {};
- sc.isExact = true;
- sc.exact = SBezier::From(ss->ctrl[0][0], ss->ctrl[0][1], ss->ctrl[0][2]);
- sc.exact.weight[1] = ss->weight[0][1];
- double max_dt = 0.5;
- if (sc.exact.deg > 1) max_dt = 0.125;
- (sc.exact).MakePwlInto(&(sc.pts), 0.0, max_dt);
- sc.surfA = revs[j];
- sc.surfB = revsp[j];
-
- hSCurve hcc = curve.AddAndAssignId(&sc);
-
- STrimBy stb;
- stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
- (surface.FindById(sc.surfA))->trim.Add(&stb);
- stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
- (surface.FindById(sc.surfB))->trim.Add(&stb);
- }
- }
- t_values.Clear();
- }
-
- hsl.Clear();
- }
-
- if(dist == 0) {
- MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
- }
-}
-
-void SShell::MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, RgbaColor color,
- Group *group) {
- int i0 = surface.n; // number of pre-existing surfaces
- SBezierLoop *sbl;
-
- if(CheckNormalAxisRelationship(sbls, pt, axis, 1.0, 0.0)) {
- axis = axis.ScaledBy(-1);
- }
-
- // Now we actually build and trim the surfaces.
- for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
- int i, j;
- SBezier *sb;
- List> hsl = {};
-
- for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
- std::vector revs(4);
- for(j = 0; j < 4; j++) {
- if(sb->deg == 1 &&
- (sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
- (sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS)
- {
- // This is a line on the axis of revolution; it does
- // not contribute a surface.
- revs[j].v = 0;
- } else {
- SSurface ss = SSurface::FromRevolutionOf(sb, pt, axis, (PI / 2) * j,
- (PI / 2) * (j + 1), 0.0, 0.0);
- ss.color = color;
- if(sb->entity != 0) {
- hEntity he;
- he.v = sb->entity;
- hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
- if(SK.entity.FindByIdNoOops(hface) != NULL) {
- ss.face = hface.v;
- }
- }
- revs[j] = surface.AddAndAssignId(&ss);
- }
- }
- hsl.Add(&revs);
- }
-
- for(i = 0; i < sbl->l.n; i++) {
- std::vector revs = hsl[i],
- revsp = hsl[WRAP(i-1, sbl->l.n)];
-
- sb = &(sbl->l[i]);
-
- for(j = 0; j < 4; j++) {
- SCurve sc;
- Quaternion qs = Quaternion::From(axis, (PI/2)*j);
- // we want Q*(x - p) + p = Q*x + (p - Q*p)
- Vector ts = pt.Minus(qs.Rotate(pt));
-
- // If this input curve generate a surface, then trim that
- // surface with the rotated version of the input curve.
- if(revs[j].v) {
- sc = {};
- sc.isExact = true;
- sc.exact = sb->TransformedBy(ts, qs, 1.0);
- (sc.exact).MakePwlInto(&(sc.pts));
- sc.surfA = revs[j];
- sc.surfB = revs[WRAP(j-1, 4)];
-
- hSCurve hcb = curve.AddAndAssignId(&sc);
-
- STrimBy stb;
- stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
- (surface.FindById(sc.surfA))->trim.Add(&stb);
- stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
- (surface.FindById(sc.surfB))->trim.Add(&stb);
- }
-
- // And if this input curve and the one after it both generated
- // surfaces, then trim both of those by the appropriate
- // circle.
- if(revs[j].v && revsp[j].v) {
- SSurface *ss = surface.FindById(revs[j]);
-
- sc = {};
- sc.isExact = true;
- sc.exact = SBezier::From(ss->ctrl[0][0],
- ss->ctrl[0][1],
- ss->ctrl[0][2]);
- sc.exact.weight[1] = ss->weight[0][1];
- (sc.exact).MakePwlInto(&(sc.pts));
- sc.surfA = revs[j];
- sc.surfB = revsp[j];
-
- hSCurve hcc = curve.AddAndAssignId(&sc);
-
- STrimBy stb;
- stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
- (surface.FindById(sc.surfA))->trim.Add(&stb);
- stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
- (surface.FindById(sc.surfB))->trim.Add(&stb);
- }
- }
- }
-
- hsl.Clear();
- }
-
- MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
-}
-
-void SShell::MakeFirstOrderRevolvedSurfaces(Vector pt, Vector axis, int i0) {
- int i;
-
- for(i = i0; i < surface.n; i++) {
- SSurface *srf = &(surface[i]);
-
- // Revolution of a line; this is potentially a plane, which we can
- // rewrite to have degree (1, 1).
- if(srf->degm == 1 && srf->degn == 2) {
- // close start, far start, far finish
- Vector cs, fs, ff;
- double d0, d1;
- d0 = (srf->ctrl[0][0]).DistanceToLine(pt, axis);
- d1 = (srf->ctrl[1][0]).DistanceToLine(pt, axis);
-
- if(d0 > d1) {
- cs = srf->ctrl[1][0];
- fs = srf->ctrl[0][0];
- ff = srf->ctrl[0][2];
- } else {
- cs = srf->ctrl[0][0];
- fs = srf->ctrl[1][0];
- ff = srf->ctrl[1][2];
- }
-
- // origin close, origin far
- Vector oc = cs.ClosestPointOnLine(pt, axis),
- of = fs.ClosestPointOnLine(pt, axis);
-
- if(oc.Equals(of)) {
- // This is a plane, not a (non-degenerate) cone.
- Vector oldn = srf->NormalAt(0.5, 0.5);
-
- Vector u = fs.Minus(of), v;
-
- v = (axis.Cross(u)).WithMagnitude(1);
-
- double vm = (ff.Minus(of)).Dot(v);
- v = v.ScaledBy(vm);
-
- srf->degm = 1;
- srf->degn = 1;
- srf->ctrl[0][0] = of;
- srf->ctrl[0][1] = of.Plus(u);
- srf->ctrl[1][0] = of.Plus(v);
- srf->ctrl[1][1] = of.Plus(u).Plus(v);
- srf->weight[0][0] = 1;
- srf->weight[0][1] = 1;
- srf->weight[1][0] = 1;
- srf->weight[1][1] = 1;
-
- if(oldn.Dot(srf->NormalAt(0.5, 0.5)) < 0) {
- swap(srf->ctrl[0][0], srf->ctrl[1][0]);
- swap(srf->ctrl[0][1], srf->ctrl[1][1]);
- }
- continue;
- }
-
- if(fabs(d0 - d1) < LENGTH_EPS) {
- // This is a cylinder; so transpose it so that we'll recognize
- // it as a surface of extrusion.
- SSurface sn = *srf;
-
- // Transposing u and v flips the normal, so reverse u to
- // flip it again and put it back where we started.
- sn.degm = 2;
- sn.degn = 1;
- int dm, dn;
- for(dm = 0; dm <= 1; dm++) {
- for(dn = 0; dn <= 2; dn++) {
- sn.ctrl [dn][dm] = srf->ctrl [1-dm][dn];
- sn.weight[dn][dm] = srf->weight[1-dm][dn];
- }
- }
-
- *srf = sn;
- continue;
- }
- }
- }
-}
-
-void SShell::MakeFromCopyOf(SShell *a) {
- ssassert(this != a, "Can't make from copy of self");
- MakeFromTransformationOf(a,
- Vector::From(0, 0, 0), Quaternion::IDENTITY, 1.0);
-}
-
-void SShell::MakeFromTransformationOf(SShell *a,
- Vector t, Quaternion q, double scale)
-{
- booleanFailed = false;
- surface.ReserveMore(a->surface.n);
- for(SSurface &s : a->surface) {
- SSurface n;
- n = SSurface::FromTransformationOf(&s, t, q, scale, /*includingTrims=*/true);
- surface.Add(&n); // keeping the old ID
- }
-
- curve.ReserveMore(a->curve.n);
- for(SCurve &c : a->curve) {
- SCurve n;
- n = SCurve::FromTransformationOf(&c, t, q, scale);
- curve.Add(&n); // keeping the old ID
- }
-}
-
-void SShell::MakeEdgesInto(SEdgeList *sel) {
- for(SSurface &s : surface) {
- s.MakeEdgesInto(this, sel, SSurface::MakeAs::XYZ);
- }
-}
-
-void SShell::MakeSectionEdgesInto(Vector n, double d, SEdgeList *sel, SBezierList *sbl)
-{
- for(SSurface &s : surface) {
- if(s.CoincidentWithPlane(n, d)) {
- s.MakeSectionEdgesInto(this, sel, sbl);
- }
- }
-}
-
-void SShell::TriangulateInto(SMesh *sm) {
-#pragma omp parallel for
- for(int i=0; iTriangulateInto(this, &m);
- #pragma omp critical
- sm->MakeFromCopyOf(&m);
- m.Clear();
- }
-}
-
-bool SShell::IsEmpty() const {
- return surface.IsEmpty();
-}
-
-void SShell::Clear() {
- for(SSurface &s : surface) {
- s.Clear();
- }
- surface.Clear();
-
- for(SCurve &c : curve) {
- c.Clear();
- }
- curve.Clear();
-}
diff --git a/src/ui.h b/src/ui.h
index 0d121b64..c4ffff2b 100644
--- a/src/ui.h
+++ b/src/ui.h
@@ -622,6 +622,7 @@ public:
void HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin,
double *wmin, bool usePerspective,
const Camera &camera);
+ void ZoomToMouse(double delta);
void LoopOverPoints(const std::vector &entities,
const std::vector &constraints,
const std::vector &faces,
@@ -842,7 +843,7 @@ public:
void MouseLeftDoubleClick(double x, double y);
void MouseMiddleOrRightDown(double x, double y);
void MouseRightUp(double x, double y);
- void MouseScroll(double x, double y, double delta);
+ void MouseScroll(double delta);
void MouseLeave();
bool KeyboardEvent(Platform::KeyboardEvent event);
void EditControlDone(const std::string &s);
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index db812bfa..eb6cf60b 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -76,6 +76,9 @@ target_link_libraries(solvespace-testsuite
solvespace-headless
${COVERAGE_LIBRARY})
+target_include_directories(solvespace-testsuite
+ PRIVATE
+ ${EIGEN3_INCLUDE_DIRS})
add_dependencies(solvespace-testsuite
resources)