Continue FreeCAD Web UI parity repairs
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled
CodeQL Advanced (c-cpp) / Analyze (c-cpp) (push) Has been cancelled
Push Crowdin Translations / update-crowdin (push) Has been cancelled
Fetch Crowdin Translations / update-crowdin (push) Has been cancelled
Build Release / upload_src (push) Has been cancelled
Build Release / build (10.13, macos-15-intel, osx-64) (push) Has been cancelled
Build Release / build (11.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (15.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (ubuntu-22.04, linux-64) (push) Has been cancelled
Build Release / build (ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Build Release / build (windows-latest, win-64) (push) Has been cancelled
Monthly issue metrics / issue metrics (push) Has been cancelled

This commit is contained in:
2026-09-23 14:03:56 +08:00
parent 2121db4358
commit 789e18ca3e
27 changed files with 48881 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" id="svg2726" version="1.1">
<defs id="defs2728">
<linearGradient id="linearGradient3860">
<stop style="stop-color:#204a87;stop-opacity:1" offset="0" id="stop3862" />
<stop style="stop-color:#729fcf;stop-opacity:1" offset="1" id="stop3864" />
</linearGradient>
<linearGradient id="linearGradient3828">
<stop style="stop-color:#729fcf;stop-opacity:1" offset="0" id="stop3830" />
<stop style="stop-color:#204a87;stop-opacity:1" offset="1" id="stop3832" />
</linearGradient>
<linearGradient id="linearGradient3766">
<stop style="stop-color:#004bed;stop-opacity:1" offset="0" id="stop3768" />
<stop style="stop-color:#bbd7ff;stop-opacity:1" offset="1" id="stop3770" />
</linearGradient>
<radialGradient xlink:href="#linearGradient3144-4" id="radialGradient3850-9" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,0.6985294,0,202.82863)" cx="225.26402" cy="672.79736" fx="225.26402" fy="672.79736" r="34.345188" />
<linearGradient id="linearGradient3144-4">
<stop style="stop-color:#ffffff;stop-opacity:1" offset="0" id="stop3146-2" />
<stop style="stop-color:#ffffff;stop-opacity:0" offset="1" id="stop3148-0" />
</linearGradient>
<linearGradient xlink:href="#linearGradient3766-6" id="linearGradient3774-3" x1="-670.94934" y1="-470.34323" x2="-296.345" y2="-470.34323" gradientUnits="userSpaceOnUse" />
<linearGradient id="linearGradient3766-6">
<stop style="stop-color:#004bed;stop-opacity:1" offset="0" id="stop3768-7" />
<stop style="stop-color:#bbd7ff;stop-opacity:1" offset="1" id="stop3770-5" />
</linearGradient>
<linearGradient xlink:href="#linearGradient3828" id="linearGradient3834" x1="-2.2241571" y1="759.33002" x2="281.50192" y2="728.49023" gradientUnits="userSpaceOnUse" />
<linearGradient xlink:href="#linearGradient3828" id="linearGradient3840" gradientUnits="userSpaceOnUse" x1="-2.2241571" y1="759.33002" x2="281.50192" y2="728.49023" />
<linearGradient xlink:href="#linearGradient3860" id="linearGradient3866" x1="35.238464" y1="33.039242" x2="27.980391" y2="20.136003" gradientUnits="userSpaceOnUse" />
</defs>
<metadata id="metadata2731">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<dc:creator>
<cc:Agent>
<dc:title>[DeepSOIC]</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>Part_Attachment</dc:title>
<dc:date>2016-05-17</dc:date>
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/Part/Gui/Resources/icons/Part_Attachment.svg</dc:identifier>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer1">
<g id="g4289" transform="matrix(0.1621282,0,0,0.1621282,6.3605986,-66.108806)">
<g transform="matrix(0,-1,-1,0,738.60196,738.60196)" id="g3888-9">
<path transform="matrix(6.1679584,0,0,6.1679584,-39.231908,407.75637)" id="path3868-1" d="M 37,15 C 28.799646,14.910168 22.4325,19.708889 17,27" style="fill:none;stroke:#a40000;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3868-2-2" d="m 188.98255,500.27574 c -50.57944,-0.55408 -89.851731,29.04423 -123.359166,74.0155" style="fill:none;stroke:#ef2929;stroke-width:12.33591652;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<g id="g3888" transform="translate(0,-24.671834)">
<path transform="matrix(6.1679584,0,0,6.1679584,-39.231908,407.75637)" id="path3868" d="M 37,15 C 28.799646,14.910168 22.4325,19.708889 17,27" style="fill:none;stroke:#a40000;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3868-2" d="m 188.98255,500.27574 c -50.57944,-0.55408 -89.851731,29.04423 -123.359166,74.0155" style="fill:none;stroke:#ef2929;stroke-width:12.33591652;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<path style="color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#0b1521;stroke-width:1.36476552;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path2998" d="m 40.88363,26.587624 a 8.8709764,8.8709764 0 1 1 -17.741953,0 8.8709764,8.8709764 0 1 1 17.741953,0 z" transform="matrix(9.0388539,0,0,9.0388539,-32.527599,266.12206)" />
<g id="g3836" transform="matrix(0.72717141,0.71036381,-0.72717141,0.71036381,555.43637,22.856283)">
<rect y="709.98633" x="-20.728033" height="61.679588" width="320.73383" id="rect3808" style="fill:#729fcf;stroke:#0b1521;stroke-width:12.13661671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.5;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:20.4" />
<rect y="722.32227" x="-8.3921156" height="37.007721" width="296.06201" id="rect3808-5" style="fill:url(#linearGradient3840);fill-opacity:1;stroke:#729fcf;stroke-width:12.13661671;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4.5;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:20.4" />
</g>
<path style="color:#000000;fill:url(#linearGradient3866);fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1.61290479;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path2998-6" d="m 40.88363,26.587624 a 8.8709764,8.8709764 0 1 1 -17.741953,0 8.8709764,8.8709764 0 1 1 17.741953,0 z" transform="matrix(7.6482608,0,0,7.6482608,11.988976,303.09463)" />
<g transform="matrix(0,-1,-1,0,787.94563,732.434)" id="g3888-9-7">
<path transform="matrix(6.1679584,0,0,6.1679584,-39.231908,407.75637)" id="path3868-1-0" d="M 37,15 C 28.799646,14.910168 15.432499,17.708888 9.9999992,24.999999" style="fill:none;stroke:#a40000;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3868-2-2-9" d="M 188.98255,500.27574 C 138.40311,499.72166 55.955106,516.98405 22.447671,561.95532" style="fill:none;stroke:#ef2929;stroke-width:12.33591652;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
</g>
<g transform="matrix(-0.58735857,0.80932682,0.80932682,0.58735857,-215.63037,164.37488)" id="g3888-9-7-3">
<path transform="matrix(6.1679584,0,0,6.1679584,-39.231908,407.75637)" id="path3868-1-0-6" d="M 31.459093,15.861765 C 23.258739,15.771933 15.432499,17.708888 9.9999992,24.999999" style="fill:none;stroke:#a40000;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3868-2-2-9-0" d="M 154.80647,505.59108 C 104.22703,505.037 55.955106,516.98405 22.447671,561.95532" style="fill:none;stroke:#ef2929;stroke-width:12.33591652;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" id="svg3612" version="1.1">
<defs id="defs3614">
<marker orient="auto" refY="0.0" refX="0.0" id="Arrow1Lstart" style="overflow:visible">
<path id="path3809" d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none" transform="scale(0.8) translate(12.5,0)" />
</marker>
<linearGradient id="linearGradient3144-6">
<stop id="stop3146-9" style="stop-color:#ffffff;stop-opacity:1" offset="0" />
<stop id="stop3148-2" style="stop-color:#ffffff;stop-opacity:0" offset="1" />
</linearGradient>
<linearGradient id="linearGradient3701">
<stop id="stop3703" style="stop-color:#ffffff;stop-opacity:1" offset="0" />
<stop id="stop3705" style="stop-color:#ffffff;stop-opacity:0" offset="1" />
</linearGradient>
<radialGradient xlink:href="#linearGradient3144-6" id="radialGradient3688" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,0.6985294,0,202.82863)" cx="225.26402" cy="672.79736" fx="225.26402" fy="672.79736" r="34.345188" />
<linearGradient id="linearGradient3708">
<stop id="stop3710" style="stop-color:#ffffff;stop-opacity:1" offset="0" />
<stop id="stop3712" style="stop-color:#ffffff;stop-opacity:0" offset="1" />
</linearGradient>
<linearGradient id="linearGradient3864-0-0">
<stop id="stop3866-5-7" offset="0" style="stop-color:#0619c0;stop-opacity:1" />
<stop id="stop3868-7-6" offset="1" style="stop-color:#379cfb;stop-opacity:1" />
</linearGradient>
<linearGradient id="linearGradient3377">
<stop id="stop3379" offset="0" style="stop-color:#ffaa00;stop-opacity:1" />
<stop id="stop3381" offset="1" style="stop-color:#faff2b;stop-opacity:1" />
</linearGradient>
<linearGradient id="linearGradient3864-0">
<stop id="stop3866-5" offset="0" style="stop-color:#0619c0;stop-opacity:1" />
<stop id="stop3868-7" offset="1" style="stop-color:#379cfb;stop-opacity:1" />
</linearGradient>
<linearGradient id="linearGradient5048">
<stop style="stop-color:black;stop-opacity:0" offset="0" id="stop5050" />
<stop id="stop5056" offset="0.5" style="stop-color:black;stop-opacity:1" />
<stop style="stop-color:black;stop-opacity:0" offset="1" id="stop5052" />
</linearGradient>
<linearGradient id="linearGradient3841-0-3">
<stop id="stop3843-1-3" offset="0" style="stop-color:#0619c0;stop-opacity:1" />
<stop id="stop3845-0-8" offset="1" style="stop-color:#379cfb;stop-opacity:1" />
</linearGradient>
<radialGradient gradientUnits="userSpaceOnUse" fy="114.5684" fx="20.892099" r="5.256" cy="114.5684" cx="20.892099" id="aigrd2">
<stop id="stop15566" style="stop-color:#F0F0F0" offset="0" />
<stop id="stop15568" style="stop-color:#9a9a9a;stop-opacity:1" offset="1.0000000" />
</radialGradient>
<radialGradient gradientUnits="userSpaceOnUse" fy="64.567902" fx="20.892099" r="5.257" cy="64.567902" cx="20.892099" id="aigrd3">
<stop id="stop15573" style="stop-color:#F0F0F0" offset="0" />
<stop id="stop15575" style="stop-color:#9a9a9a;stop-opacity:1" offset="1.0000000" />
</radialGradient>
<linearGradient id="linearGradient15662">
<stop style="stop-color:#ffffff;stop-opacity:1" offset="0.0000000" id="stop15664" />
<stop style="stop-color:#f8f8f8;stop-opacity:1" offset="1.0000000" id="stop15666" />
</linearGradient>
<radialGradient r="86.70845" fy="35.736916" fx="33.966679" cy="35.736916" cx="33.966679" gradientTransform="matrix(0.96049297,0,0,1.041132,-52.144249,-702.33158)" gradientUnits="userSpaceOnUse" id="radialGradient4452" xlink:href="#linearGradient259" />
<linearGradient id="linearGradient259">
<stop style="stop-color:#fafafa;stop-opacity:1" offset="0.0000000" id="stop260" />
<stop style="stop-color:#bbbbbb;stop-opacity:1" offset="1.0000000" id="stop261" />
</linearGradient>
<radialGradient r="37.751713" fy="3.7561285" fx="8.824419" cy="3.7561285" cx="8.824419" gradientTransform="matrix(0.96827297,0,0,1.032767,-48.790699,-701.68513)" gradientUnits="userSpaceOnUse" id="radialGradient4454" xlink:href="#linearGradient269" />
<linearGradient id="linearGradient269">
<stop style="stop-color:#a3a3a3;stop-opacity:1" offset="0.0000000" id="stop270" />
<stop style="stop-color:#4c4c4c;stop-opacity:1" offset="1.0000000" id="stop271" />
</linearGradient>
<linearGradient id="linearGradient4095">
<stop style="stop-color:#005bff;stop-opacity:1" offset="0" id="stop4097" />
<stop style="stop-color:#c1e3f7;stop-opacity:1" offset="1" id="stop4099" />
</linearGradient>
<linearGradient xlink:href="#linearGradient4247" id="linearGradient4253" x1="394.15784" y1="185.1304" x2="434.73947" y2="140.22731" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.94231826,0,0,0.94231826,23.727549,8.8262536)" />
<linearGradient id="linearGradient4247">
<stop style="stop-color:#2e8207;stop-opacity:1" offset="0" id="stop4249" />
<stop style="stop-color:#52ff00;stop-opacity:1" offset="1" id="stop4251" />
</linearGradient>
<linearGradient y2="140.22731" x2="434.73947" y1="185.1304" x1="394.15784" gradientTransform="matrix(0.94231826,0,0,0.94231826,23.727549,8.8262536)" gradientUnits="userSpaceOnUse" id="linearGradient5087" xlink:href="#linearGradient4247" />
</defs>
<g id="layer1">
<g id="g931" transform="translate(-5.0212139,-6)">
<path id="path3953-1" d="m 20.999999,19 h 4" style="fill:none;stroke:#204a87;stroke-width:5.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3957-5" d="M 53.000001,19 C 57,19 57,19 57,19" style="fill:none;stroke:#204a87;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-1" d="m 20.999999,19 h 4" style="fill:none;stroke:#729fcf;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3957-5-8" d="M 53.000001,19 C 57,19 57,19 57,19" style="fill:none;stroke:#729fcf;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-5" d="m 37,19 h 4" style="fill:none;stroke:#204a87;stroke-width:5.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-1-7" d="m 37,19 h 4" style="fill:none;stroke:#729fcf;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-6" d="m 19,57 h 4" style="fill:none;stroke:#204a87;stroke-width:5.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3957-5-1" d="m 51.000002,57 c 3.999999,0 3.999999,0 3.999999,0" style="fill:none;stroke:#204a87;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-1-8" d="m 19,57 h 4" style="fill:none;stroke:#729fcf;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3957-5-8-9" d="m 51.000002,57 c 3.999999,0 3.999999,0 3.999999,0" style="fill:none;stroke:#729fcf;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-5-2" d="m 35.000001,57 h 4" style="fill:none;stroke:#204a87;stroke-width:5.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-1-7-7" d="m 35.000001,57 h 4" style="fill:none;stroke:#729fcf;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-9" d="M 17,26.999999 V 31" style="fill:none;stroke:#204a87;stroke-width:5.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-1-4" d="M 17,26.999999 V 31" style="fill:none;stroke:#729fcf;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-5-1" d="M 17,43.000001 V 47" style="fill:none;stroke:#204a87;stroke-width:5.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-1-7-2" d="M 17,43.000001 V 47" style="fill:none;stroke:#729fcf;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-9-3" d="M 57,28.999999 V 33" style="fill:none;stroke:#204a87;stroke-width:5.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-1-4-3" d="M 57,28.999999 V 33" style="fill:none;stroke:#729fcf;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-5-1-4" d="M 57,45.000001 V 49" style="fill:none;stroke:#204a87;stroke-width:5.99999952;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3953-1-1-7-2-1" d="M 57,45.000001 V 49" style="fill:none;stroke:#729fcf;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</g>
<metadata id="metadata5469">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<cc:license rdf:resource="" />
<dc:date>Mon Oct 10 13:44:52 2011 +0000</dc:date>
<dc:creator>
<cc:Agent>
<dc:title>[wmayer]</dc:title>
</cc:Agent>
</dc:creator>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/Draft/Resources/icons/Draft_SelectGroup.svg</dc:identifier>
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
<dc:subject>
<rdf:Bag>
<rdf:li>hierarchy</rdf:li>
<rdf:li>group</rdf:li>
<rdf:li>selection</rdf:li>
<rdf:li>tree</rdf:li>
</rdf:Bag>
</dc:subject>
<dc:description>A hierarchical tree structure with two blue child elements of a white parent element, both of which are surrounded by the same dotted box</dc:description>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,281 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="svg2985"
height="64px"
width="64px">
<title
id="title889">Part_PointsFromMesh</title>
<defs
id="defs2987">
<linearGradient
id="linearGradient4387">
<stop
id="stop4389"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop4391"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient6321">
<stop
id="stop6323"
offset="0"
style="stop-color:#71b2f8;stop-opacity:1;" />
<stop
id="stop6325"
offset="1"
style="stop-color:#002795;stop-opacity:1;" />
</linearGradient>
<radialGradient
gradientTransform="translate(-0.23443224,0.23443198)"
gradientUnits="userSpaceOnUse"
r="19.467436"
fy="28.869568"
fx="45.883327"
cy="28.869568"
cx="45.883327"
id="radialGradient3692"
xlink:href="#linearGradient3377" />
<linearGradient
id="linearGradient3377">
<stop
style="stop-color:#faff2b;stop-opacity:1;"
offset="0"
id="stop3379" />
<stop
style="stop-color:#ffaa00;stop-opacity:1;"
offset="1"
id="stop3381" />
</linearGradient>
<linearGradient
id="linearGradient3377-3">
<stop
style="stop-color:#faff2b;stop-opacity:1;"
offset="0"
id="stop3379-8" />
<stop
style="stop-color:#ffaa00;stop-opacity:1;"
offset="1"
id="stop3381-3" />
</linearGradient>
<radialGradient
r="19.467436"
fy="28.869568"
fx="45.883327"
cy="28.869568"
cx="45.883327"
gradientTransform="matrix(0.67067175,0,0,0.64145918,-63.380792,0.83845403)"
gradientUnits="userSpaceOnUse"
id="radialGradient6412"
xlink:href="#linearGradient3377-3" />
<linearGradient
xlink:href="#linearGradient3036"
id="linearGradient3049"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(33.834399,20.280412,56.143946)"
x1="14.824193"
y1="50.468616"
x2="20.93985"
y2="56.000233" />
<linearGradient
id="linearGradient3036">
<stop
style="stop-color:#ef2929;stop-opacity:1"
offset="0"
id="stop3038" />
<stop
style="stop-color:#a40000;stop-opacity:1"
offset="1"
id="stop3040" />
</linearGradient>
</defs>
<metadata
id="metadata2990">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Part_PointsFromMesh</dc:title>
<dc:creator>
<cc:Agent>
<dc:title>[bitacovir]</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>Part_Shape_from_Mesh</dc:title>
<dc:date>2021-03-21</dc:date>
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier />
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title>Based on Alexander Gryson's work</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
<g
style="stroke-width:3.5;stroke-miterlimit:4;stroke-dasharray:none"
id="g3042"
transform="matrix(0.67067175,0,0,0.55048347,-120.90697,-52.046252)">
<path
id="path3150-7-4"
d="m 226.49973,114.52887 v 36.3317 l -17.89251,14.53268 v -36.33171 z"
style="fill:#4e9a06;stroke:none" />
<path
id="path3930-0"
d="M 208.60722,161.76008 226.49973,147.2274"
style="fill:none;stroke:#73d216;stroke-width:3.29157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3932-9"
d="m 223.51764,154.49374 v -36.3317"
style="fill:none;stroke:#73d216;stroke-width:3.29157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3934-4"
d="m 226.49973,118.16204 -17.89251,14.53267"
style="fill:none;stroke:#73d216;stroke-width:3.29157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3936-8"
d="m 211.5893,125.42838 v 36.3317"
style="fill:none;stroke:#73d216;stroke-width:3.29157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3152-1-8"
d="m 208.60722,165.39325 -23.85668,-14.53268 v -36.3317 l 23.85668,14.53267 z"
style="fill:#73d216;stroke:none" />
<path
id="path3938-2"
d="m 205.62513,129.06154 v 32.69854"
style="fill:none;stroke:#8ae234;stroke-width:3.29157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3940-4"
d="m 184.75054,147.2274 23.85668,14.53268"
style="fill:none;stroke:#8ae234;stroke-width:3.29157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3942-5"
d="M 187.73263,150.86057 V 118.16204"
style="fill:none;stroke:#8ae234;stroke-width:3.29157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3150-5"
d="m 226.49973,114.52887 v 36.3317 l -17.89251,14.53268 v -36.33171 z"
style="fill:none;stroke:#172a04;stroke-width:3.29157;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3944-1"
d="M 208.60722,132.69471 184.75054,118.16204"
style="fill:none;stroke:#8ae234;stroke-width:3.29157;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3152-7"
d="m 208.60722,165.39325 -23.85668,-14.53268 v -36.3317 l 23.85668,14.53267 z"
style="fill:none;stroke:#172a04;stroke-width:3.29157;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3156-1"
d="m 226.49973,114.52887 -17.89251,14.53267 -23.85668,-14.53267 17.89251,-14.532675 23.85668,14.532675"
style="fill:#8ae234;stroke:#172a04;stroke-width:3.29157;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3928-5"
d="m 184.75054,114.52887 h 41.74919"
style="fill:none;stroke:#172a04;stroke-width:3.29157;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3928-5-3"
d="m 184.75054,150.86057 23.85668,-21.79902"
style="fill:none;stroke:#172a04;stroke-width:3.29157;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3928-5-6"
d="m 208.60722,165.39325 17.89251,-50.86438"
style="fill:none;stroke:#172a04;stroke-width:3.29157;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<path
style="display:inline;overflow:visible;visibility:visible;fill:url(#linearGradient3049);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.2;marker:none;enable-background:accumulate"
d="m 14.577858,56.963669 14.422159,0.03633 -5.51345,-13.326736 -2.227176,3.322601 -9.967806,-6.681533 -4.4543552,6.645202 9.9678062,6.681534 z"
id="rect3165" />
<path
style="fill:none;stroke:#ef2929;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
d="m 18.466336,54.754657 1.113588,-1.6613 -9.9678041,-6.681534 3.3407661,-4.983901"
id="path4087" />
<path
style="fill:none;stroke:#ef2929;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 15.768199,54.861321 12.089968,0.0052"
id="path4089" />
<path
style="fill:none;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 17.918624,51.979768 0.547712,2.774889"
id="path4091" />
<path
style="fill:none;stroke:#ef2929;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 27.041007,56.356585 22.755847,45.99721"
id="path4042" />
<path
style="display:inline;overflow:visible;visibility:visible;fill:none;stroke:#280000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
d="m 15.058839,56.727355 14.422158,0.03633 -5.51345,-13.326735 -2.227175,3.322601 -9.967805,-6.681534 -4.4543552,6.645203 9.9678052,6.681533 z"
id="rect3165-1" />
<ellipse
id="path3896-0-1-8"
style="display:inline;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="44.866348"
cy="26.883219"
rx="1.9999998"
ry="2.0000005" />
<ellipse
ry="2.0000005"
rx="1.9999998"
cy="37.621815"
cx="58.983444"
style="display:inline;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path3896-0-1-8-6" />
<ellipse
ry="2.0000005"
rx="1.9999998"
cy="44.767174"
cx="48.710922"
style="display:inline;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path3896-0-1-8-4" />
<ellipse
ry="2.0000005"
rx="1.9999998"
cy="51.824265"
cx="34.651279"
style="display:inline;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path3896-0-1-8-9" />
<ellipse
ry="2.0000005"
rx="1.9999998"
cy="35.536747"
cx="34.120564"
style="display:inline;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path3896-0-1-8-92" />
<ellipse
ry="2.0000005"
rx="1.9999998"
cy="58.979542"
cx="49.133045"
style="display:inline;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path3896-0-1-8-41" />
<ellipse
ry="2.0000005"
rx="1.9999998"
cy="52.966244"
cx="58.995705"
style="display:inline;fill:#d3d7cf;fill-opacity:1;fill-rule:nonzero;stroke:#2e3436;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path3896-0-1-8-3" />
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" id="svg2985" version="1.1">
<defs id="defs2987">
<linearGradient id="linearGradient4387">
<stop style="stop-color:#71b2f8;stop-opacity:1" offset="0" id="stop4389" />
<stop style="stop-color:#002795;stop-opacity:1" offset="1" id="stop4391" />
</linearGradient>
<linearGradient id="linearGradient6321">
<stop style="stop-color:#71b2f8;stop-opacity:1" offset="0" id="stop6323" />
<stop style="stop-color:#002795;stop-opacity:1" offset="1" id="stop6325" />
</linearGradient>
<radialGradient xlink:href="#linearGradient3377" id="radialGradient3692" cx="45.883327" cy="28.869568" fx="45.883327" fy="28.869568" r="19.467436" gradientUnits="userSpaceOnUse" gradientTransform="translate(-0.23443224,0.23443198)" />
<linearGradient id="linearGradient3377">
<stop id="stop3379" offset="0" style="stop-color:#faff2b;stop-opacity:1" />
<stop id="stop3381" offset="1" style="stop-color:#ffaa00;stop-opacity:1" />
</linearGradient>
<linearGradient id="linearGradient3377-3">
<stop id="stop3379-8" offset="0" style="stop-color:#faff2b;stop-opacity:1" />
<stop id="stop3381-3" offset="1" style="stop-color:#ffaa00;stop-opacity:1" />
</linearGradient>
<radialGradient xlink:href="#linearGradient3377-3" id="radialGradient6412" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.67067175,0,0,0.64145918,-63.380792,0.83845403)" cx="45.883327" cy="28.869568" fx="45.883327" fy="28.869568" r="19.467436" />
<linearGradient y2="56.000233" x2="20.93985" y1="50.468616" x1="14.824193" gradientTransform="matrix(0.83065033,0.55679442,-0.55679442,0.83065033,34.695117,-1.7840615)" gradientUnits="userSpaceOnUse" id="linearGradient3049" xlink:href="#linearGradient3036" />
<linearGradient id="linearGradient3036">
<stop id="stop3038" offset="0" style="stop-color:#ef2929;stop-opacity:1" />
<stop id="stop3040" offset="1" style="stop-color:#a40000;stop-opacity:1" />
</linearGradient>
</defs>
<metadata id="metadata2990">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<dc:creator>
<cc:Agent>
<dc:title>[jmaustpc]</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>Part_Shape_from_Mesh</dc:title>
<dc:date>2013-12-13</dc:date>
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/Part/Gui/Resources/icons/Part_Shape_from_Mesh.svg</dc:identifier>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer1">
<g id="g3038" transform="matrix(1,0,0,0.85817381,37.193787,2.8828554)">
<g style="stroke-width:3.5;stroke-miterlimit:4;stroke-dasharray:none" transform="matrix(0.67067175,0,0,0.64145918,-128.10074,-38.37114)" id="g3618-3">
<g id="g3060" transform="translate(44.731271,39.96487)">
<path id="path3150-7" d="m 181.76846,74.564006 0,36.331694 -17.89251,14.53268 0,-36.3317 z" style="fill:#204a87;stroke:none" />
<path id="path3930" d="m 163.87595,121.79521 17.89251,-14.53268" style="fill:none;stroke:#3465a4;stroke-width:3.29156828;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3932" d="m 178.78637,114.52887 0,-36.331697" style="fill:none;stroke:#3465a4;stroke-width:3.29156828;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3934" d="m 181.76846,78.197173 -17.89251,14.53268" style="fill:none;stroke:#3465a4;stroke-width:3.29156828;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3936" d="m 166.85803,85.463513 0,36.331697" style="fill:none;stroke:#3465a4;stroke-width:3.29156828;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3152-1" d="m 163.87595,125.42838 -23.85668,-14.53268 0,-36.331697 23.85668,14.532677 z" style="fill:#3465a4;stroke:none" />
<path id="path3938" d="m 160.89386,89.096683 0,32.698527" style="fill:none;stroke:#729fcf;stroke-width:3.29156828;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3940" d="m 140.01927,107.26253 23.85668,14.53268" style="fill:none;stroke:#729fcf;stroke-width:3.29156828;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3942" d="m 143.00136,110.8957 0,-32.698527" style="fill:none;stroke:#729fcf;stroke-width:3.29156828;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3150" d="m 181.76846,74.564003 0,36.331697 -17.89251,14.53268 0,-36.331697 z" style="fill:none;stroke:#0b1521;stroke-width:3.29156828;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
<path id="path3944" d="M 163.87595,92.729853 140.01927,78.197173" style="fill:none;stroke:#729fcf;stroke-width:3.29156828;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path id="path3152" d="m 163.87595,125.42838 -23.85668,-14.53268 0,-36.331697 23.85668,14.53268 z" style="fill:none;stroke:#0b1521;stroke-width:3.29156828;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
<path id="path3156" d="m 181.76846,74.564003 -17.89251,14.53268 -23.85668,-14.53268 17.89251,-14.53268 23.85668,14.53268" style="fill:#729fcf;stroke:#0b1521;stroke-width:3.29156828;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
</g>
<g id="g3042" transform="translate(-44.731295,-39.96487)">
<path id="path3150-7-4" d="m 226.49973,114.52887 0,36.3317 -17.89251,14.53268 0,-36.33171 z" style="fill:#4e9a06;stroke:none" />
<path id="path3930-0" d="M 208.60722,161.76008 226.49973,147.2274" style="fill:none;stroke:#73d216;stroke-width:3.29156804;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3932-9" d="m 223.51764,154.49374 0,-36.3317" style="fill:none;stroke:#73d216;stroke-width:3.29156804;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3934-4" d="m 226.49973,118.16204 -17.89251,14.53267" style="fill:none;stroke:#73d216;stroke-width:3.29156804;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3936-8" d="m 211.5893,125.42838 0,36.3317" style="fill:none;stroke:#73d216;stroke-width:3.29156804;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3152-1-8" d="m 208.60722,165.39325 -23.85668,-14.53268 0,-36.3317 23.85668,14.53267 z" style="fill:#73d216;stroke:none" />
<path id="path3938-2" d="m 205.62513,129.06154 0,32.69854" style="fill:none;stroke:#8ae234;stroke-width:3.29156804;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3940-4" d="m 184.75054,147.2274 23.85668,14.53268" style="fill:none;stroke:#8ae234;stroke-width:3.29156804;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3942-5" d="m 187.73263,150.86057 0,-32.69853" style="fill:none;stroke:#8ae234;stroke-width:3.29156804;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3150-5" d="m 226.49973,114.52887 0,36.3317 -17.89251,14.53268 0,-36.33171 z" style="fill:none;stroke:#172a04;stroke-width:3.29156804;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3944-1" d="M 208.60722,132.69471 184.75054,118.16204" style="fill:none;stroke:#8ae234;stroke-width:3.29156804;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3152-7" d="m 208.60722,165.39325 -23.85668,-14.53268 0,-36.3317 23.85668,14.53267 z" style="fill:none;stroke:#172a04;stroke-width:3.29156804;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3156-1" d="m 226.49973,114.52887 -17.89251,14.53267 -23.85668,-14.53267 17.89251,-14.532675 23.85668,14.532675" style="fill:#8ae234;stroke:#172a04;stroke-width:3.29156804;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3928-5" d="m 184.75054,114.52887 41.74919,0" style="fill:none;stroke:#172a04;stroke-width:3.29156804;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3928-5-3" d="m 184.75054,150.86057 23.85668,-21.79902" style="fill:none;stroke:#172a04;stroke-width:3.29156804;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path id="path3928-5-6" d="m 208.60722,165.39325 17.89251,-50.86438" style="fill:none;stroke:#172a04;stroke-width:3.29156804;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
</g>
</g>
</g>
<path style="fill:url(#linearGradient3049);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.20000005;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" d="m 14.577858,56.963669 14.422159,0.03633 -5.51345,-13.326736 -2.227176,3.322601 -9.967806,-6.681533 -4.4543552,6.645202 9.9678062,6.681534 z" id="rect3165" />
<path style="fill:none;stroke:#ef2929;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" d="m 18.466336,54.754657 1.113588,-1.6613 -9.9678041,-6.681534 3.3407661,-4.983901" id="path4087" />
<path style="fill:none;stroke:#ef2929;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 15.768199,54.861321 12.089968,0.0052" id="path4089" />
<path style="fill:none;stroke:#ef2929;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 17.918624,51.979768 0.547712,2.774889" id="path4091" />
<path style="fill:none;stroke:#ef2929;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 27.041007,56.356585 22.755847,45.99721" id="path4042" />
<path style="fill:none;stroke:#280000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" d="m 15.058839,56.727355 14.422158,0.03633 -5.51345,-13.326735 -2.227175,3.322601 -9.967805,-6.681534 -4.4543552,6.645203 9.9678052,6.681533 z" id="rect3165-1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="64px" height="64px" id="svg5821" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs id="defs5823">
<linearGradient id="linearGradient3776">
<stop style="stop-color:#cc0000;stop-opacity:1" offset="0" id="stop3778" />
<stop style="stop-color:#ef2929;stop-opacity:1" offset="1" id="stop3780" />
</linearGradient>
<linearGradient id="linearGradient3768">
<stop style="stop-color:#cc0000;stop-opacity:1" offset="0" id="stop3770" />
<stop style="stop-color:#ef2929;stop-opacity:1" offset="1" id="stop3772" />
</linearGradient>
<linearGradient id="linearGradient6349">
<stop style="stop-color:#000000;stop-opacity:1" offset="0" id="stop6351" />
<stop style="stop-color:#000000;stop-opacity:0" offset="1" id="stop6353" />
</linearGradient>
<linearGradient id="linearGradient3377">
<stop style="stop-color:#0019a3;stop-opacity:1" offset="0" id="stop3379" />
<stop style="stop-color:#0069ff;stop-opacity:1" offset="1" id="stop3381" />
</linearGradient>
<linearGradient xlink:href="#linearGradient3377" id="linearGradient3383" x1="901.1875" y1="1190.875" x2="1267.9062" y2="1190.875" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1,0,0,1,2199.356,0)" />
<radialGradient xlink:href="#linearGradient6349" id="radialGradient6355" cx="1103.6399" cy="1424.4465" fx="1103.6399" fy="1424.4465" r="194.40614" gradientTransform="matrix(-1.4307499,-1.3605156e-7,-1.202713e-8,0.1264801,2674.7488,1244.2826)" gradientUnits="userSpaceOnUse" />
<linearGradient xlink:href="#linearGradient3768" id="linearGradient3774" x1="1178.153" y1="1453.8708" x2="1105.0463" y2="1059.0945" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.70270076,0,0,0.78787889,315.88365,269.84952)" />
<linearGradient xlink:href="#linearGradient3776" id="linearGradient3782" x1="33.052631" y1="73.676765" x2="23.483253" y2="19.131313" gradientUnits="userSpaceOnUse" />
<linearGradient gradientTransform="matrix(7.3106737,0,0,7.3106737,841.86203,1044.4732)" xlink:href="#linearGradient3874" id="linearGradient3880" x1="56" y1="47" x2="53" y2="39" gradientUnits="userSpaceOnUse" />
<linearGradient id="linearGradient3874">
<stop style="stop-color:#555753;stop-opacity:1" offset="0" id="stop3876" />
<stop style="stop-color:#babdb6;stop-opacity:1" offset="1" id="stop3878" />
</linearGradient>
<linearGradient xlink:href="#linearGradient3866" id="linearGradient3872" x1="35" y1="53" x2="24" y2="9" gradientUnits="userSpaceOnUse" />
<linearGradient id="linearGradient3866">
<stop style="stop-color:#d3d7cf;stop-opacity:1" offset="0" id="stop3868" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop3870" />
</linearGradient>
<linearGradient gradientTransform="matrix(0,7.3106737,7.3106737,0,900.34735,985.98777)" y2="20.000008" x2="6.000001" y1="38.000008" x1="58" gradientUnits="userSpaceOnUse" id="linearGradient3127" xlink:href="#linearGradient3866" />
</defs>
<metadata id="metadata5826">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>[wmayer]</dc:title>
</cc:Agent>
</dc:creator>
<dc:date>2011-10-10</dc:date>
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/Sketcher/Gui/Resources/icons/Sketcher_NewSketch.svg</dc:identifier>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer1">
<g id="g3360" transform="matrix(0.1367863,0,0,0.1367863,-119.15519,-134.86962)">
<path d="m 922.27941,1007.9198 365.53359,0 0,350.9123 -73.1067,73.1069 -292.42689,0 z" id="rect3005" style="color:#000000;fill:url(#linearGradient3127);fill-opacity:1;fill-rule:evenodd;stroke:#3f3f3f;stroke-width:14.62134743;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path style="fill:none;stroke:#ffffff;stroke-width:14.62134743;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 1273.1917,1022.5412 -336.29092,0 0,394.7763 271.22592,0 65.065,-66.5271 z" id="path3096" />
<path d="m 1287.8132,1358.8322 c 0,-7.3107 -29.2428,-29.2427 -58.4855,-29.2427 0,0 14.6213,73.1067 -14.6214,102.3494 z" id="path3778" style="color:#000000;fill:url(#linearGradient3880);fill-opacity:1;fill-rule:evenodd;stroke:#3f3f3f;stroke-width:14.26466179;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path style="fill:none;stroke:#babdb6;stroke-width:14.62134743;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 1229.9528,1396.295 48.6777,-47.1803" id="path3125" />
<path d="m 1287.8132,1358.8322 c 0,-7.3107 -29.2428,-29.2427 -58.4855,-29.2427 0,0 14.6213,73.1067 -14.6214,102.3494 z" id="path3778-3" style="color:#000000;fill:none;stroke:#3f3f3f;stroke-width:14.26466179;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect style="color:#000000;fill:none;stroke:url(#linearGradient3774);stroke-width:29.24269676;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="rect3860-3" width="190.077" height="190.07753" x="958.83331" y="1190.6866" />
<path style="color:#000000;fill:none;stroke:url(#linearGradient3782);stroke-width:5.53487444;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path3862-6" d="m 48.363636,33.272728 c 0,10.041541 -8.140277,18.181818 -18.181818,18.181818 C 20.140277,51.454546 12,43.314269 12,33.272728 12,23.231187 20.140277,15.09091 30.181818,15.09091 c 10.041541,0 18.181818,8.140277 18.181818,18.181818 z" transform="matrix(5.4281292,0,0,5.1424383,996.04635,999.19155)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,204 @@
import React, { useEffect, useRef } from "react";
import { PART_DESKTOP_MENUS } from "./part-desktop-adapter.js";
import { ToolbarGroup } from "./persistent-toolbar-workspace.js";
export interface PartUiAction {
command: string; label: string; task: string; icon?: string;
primitive?: "Cylinder" | "Sphere" | "Cone" | "Torus";
boolean?: "Cut" | "Fuse" | "Common";
join?: "connect" | "embed" | "cutout";
split?: "fragments" | "slice-apart" | "slice" | "xor";
}
// Interaction mapping only. Geometry and document mutations stay in the existing adapters.
// Menu families come from the existing Part desktop adapter. Native command labels/icons
// correspond to Part/Gui/Command.cpp, CommandParametric.cpp and Resources/Part.qrc.
const rows: Array<[string, string, string, string?, Partial<PartUiAction>?]> = [
["Part_Box", "Cube", "box", "Part_Box_Parametric"],
["Part_Cylinder", "Cylinder", "primitive", "Part_Cylinder_Parametric", { primitive: "Cylinder" }],
["Part_Sphere", "Sphere", "primitive", "Part_Sphere_Parametric", { primitive: "Sphere" }],
["Part_Cone", "Cone", "primitive", "Part_Cone_Parametric", { primitive: "Cone" }],
["Part_Torus", "Torus", "primitive", "Part_Torus_Parametric", { primitive: "Torus" }],
["Part_Tube", "Tube", "tube", "Part_Tube"],
["Part_Primitives", "Primitive", "primitives", "Part_Primitives"],
["Part_ToleranceSet", "Set Tolerance", "tolerance", "Part_ToleranceSet"],
["Part_Builder", "Shape Builder", "shape-builder", "Part_Shapebuilder"],
["Part_SimpleCopy", "Simple Copy", "simple-copy", "Part_3D_object"],
["Part_TransformedCopy", "Transformed Copy", "transformed-copy", "Part_Transformed_Copy"],
["Part_ElementCopy", "Element Copy", "element-copy", "Part_Element_Copy"],
["Part_RefineShape", "Refine Shape", "refine-shape", "Part_Refine_Shape"],
["Part_Boolean", "Boolean Operation", "boolean", "Part_Booleans"],
["Part_Cut", "Cut", "boolean", "Part_Cut", { boolean: "Cut" }],
["Part_Fuse", "Union", "boolean", "Part_Fuse", { boolean: "Fuse" }],
["Part_Common", "Intersection", "boolean", "Part_Common", { boolean: "Common" }],
["Part_JoinConnect", "Connect Shapes", "join", "Part_JoinConnect", { join: "connect" }],
["Part_JoinEmbed", "Embed Shapes", "join", "Part_JoinEmbed", { join: "embed" }],
["Part_JoinCutout", "Cutout for Object", "join", "Part_JoinCutout", { join: "cutout" }],
["Part_BooleanFragments", "Boolean Fragments", "split", "Part_BooleanFragments", { split: "fragments" }],
["Part_SliceApart", "Slice Apart", "split", "Part_SliceApart", { split: "slice-apart" }],
["Part_Slice", "Slice to Compound", "split", "Part_Slice", { split: "slice" }],
["Part_XOR", "Boolean XOR", "split", "Part_XOR", { split: "xor" }],
["Part_Compound", "Compound", "compound", "Part_Compound"],
["Part_ExplodeCompound", "Explode Compound", "explode-compound", "Part_ExplodeCompound"],
["Part_CompoundFilter", "Compound Filter", "compound-filter", "Part_CompoundFilter"],
["Part_Extrude", "Extrude", "extrude", "Part_Extrude"],
["Part_Revolve", "Revolve", "revolve", "Part_Revolve"],
["Part_Mirror", "Mirror", "mirror", "Part_Mirror"],
["Part_Scale", "Scale", "scale", "Part_Scale"],
["Part_Fillet", "Fillet", "fillet", "Part_Fillet"],
["Part_Chamfer", "Chamfer", "chamfer", "Part_Chamfer"],
["Part_MakeFace", "Face From Wires", "make-face", "Part_MakeFace"],
["Part_RuledSurface", "Ruled Surface", "ruled-surface", "Part_RuledSurface"],
["Part_Loft", "Loft", "loft", "Part_Loft"],
["Part_Sweep", "Sweep", "sweep", "Part_Sweep"],
["Part_Section", "Section", "section", "Part_Section"],
["Part_CrossSections", "Cross-Sections", "cross-sections", "Part_CrossSections"],
["Part_Offset", "3D Offset", "offset", "Part_Offset"],
["Part_Offset2D", "2D Offset", "offset2d", "Part_Offset2D"],
["Part_Thickness", "Thickness", "thickness", "Part_Thickness"],
["Part_ProjectionOnSurface", "Project on Surface", "projection-on-surface", "Part_ProjectionOnSurface"],
["Part_SectionCut", "Persistent Section Cut", "section-cut", "Part_SectionCut"],
["Part_CheckGeometry", "Check Geometry", "check-geometry", "Part_CheckGeometry"],
["Part_Defeaturing", "Defeaturing", "defeaturing", "Part_Defeaturing"],
["Part_MakeSolid", "Convert to Solid", "make-solid", "Part_MakeSolid"],
["Part_ReverseShape", "Reverse Shapes", "reverse-shapes", "Part_Reverse_Shape"],
["Part_ColorPerFace", "Appearance per Face", "face-colors", "Part_ColorFace"],
["Part_BoxSelection", "Box Selection", "box-selection", "Part_BoxSelection"],
["Part_ShapeFromMesh", "Shape From Mesh", "shape-from-mesh", "Part_Shape_from_Mesh"],
["Part_PointsFromMesh", "Points From Shape", "points-from-mesh", "Part_PointsFromMesh"],
["Part_EditAttachment", "Attachment", "edit-attachment", "Part_Attachment"],
["Sketcher_NewSketch", "New Sketch", "sketcher-new-sketch", "Sketcher_NewSketch"],
["Materials_InspectAppearance", "Inspect Appearance", "inspect-appearance"],
["Materials_InspectMaterial", "Inspect Material", "inspect-material"],
];
export const partUiActions = Object.fromEntries(rows.map(([command, label, task, icon, options]) =>
[command, { command, label, task, icon, ...options }])) as Record<string, PartUiAction>;
const family = (name: string) => (PART_DESKTOP_MENUS[name] ?? []).filter(command => partUiActions[command]);
const tools = ["Part_Extrude", "Part_Revolve", "Part_Mirror", "Part_Scale", "Part_Fillet", "Part_Chamfer", "Part_MakeFace", "Part_RuledSurface", "Part_Loft", "Part_Sweep", "Part_Section", "Part_CrossSections"];
export const partUiCss = `
.part-menu{position:relative}.part-menu>summary{list-style:none}.part-menu>summary::-webkit-details-marker{display:none}
.part-command-menu{position:absolute;left:0;top:100%;z-index:50;min-width:240px;padding:3px;background:#17212b;color:#e7edf3;border:1px solid #435464;box-shadow:0 4px 12px #0006}
.part-command-menu .part-submenu{position:relative}.part-command-menu .part-submenu>.part-command-menu{left:100%;top:0}
.part-command-menu button,.part-command-menu summary{font:inherit;font-size:13px;display:flex;align-items:center;gap:8px;width:100%;padding:4px 8px;border:0;border-radius:0;background:transparent;color:inherit;white-space:nowrap;text-align:left;cursor:default}
.part-command-menu summary{list-style:none}.part-command-menu summary::after{content:'▸';margin-left:auto}
.part-command-menu button:hover,.part-command-menu button:focus-visible,.part-command-menu summary:hover,.part-command-menu summary:focus-visible{background:#264b56;outline:1px solid #66c7d5;outline-offset:-1px}
.part-command-menu button:disabled{opacity:.45}.part-command-menu hr{border:0;border-top:1px solid #435464;margin:3px 2px}
.part-command-menu img,.part-command-icon{width:20px;height:20px;object-fit:contain;flex-shrink:0}.part-command-menu summary{padding-left:36px}.part-tool-button{display:inline-flex;align-items:center;justify-content:center;background:transparent;border:1px solid transparent;border-radius:2px;color:inherit;padding:3px;min-width:30px;height:30px;cursor:pointer}
.part-tool-button img{width:24px;height:24px}.part-tool-button:hover,.part-tool-button:focus-visible{border-color:#66c7d5;background:#264b56}.part-tool-button[aria-pressed=true]{border-color:#66c7d5}.part-tool-button:disabled{opacity:.4;cursor:default}
.part-tool-group{position:relative}.part-tool-group>summary{list-style:none;font-size:12px;padding:4px;cursor:pointer}.part-tool-group>summary::after{content:' ▾'}.part-tool-group>.part-command-menu{top:100%}
@media(max-width:650px){.part-command-menu .part-submenu>.part-command-menu{position:static;box-shadow:none;border:0;padding-left:14px}.part-menu>.part-command-menu{max-height:70vh;overflow:auto}}
`;
type Props = { disabled(action: PartUiAction): boolean; activate(action: PartUiAction): void; currentTask: string };
function closeMenu(node: HTMLElement, focus = false) {
const root = node.closest<HTMLDetailsElement>('.part-menu, .part-tool-group, .application-menus > details');
root?.querySelectorAll<HTMLDetailsElement>('details[open]').forEach(item => { item.open = false; });
if (root) { root.open = false; if (focus) root.querySelector<HTMLElement>('summary')?.focus(); }
}
function menuItems(menu: HTMLElement) {
return [...menu.querySelectorAll<HTMLElement>('button:not(:disabled), summary')]
.filter(item => item.closest('.part-command-menu') === menu);
}
function openGroup(group: HTMLDetailsElement, focus: 'first' | 'last' | false = false) {
const siblings = group.parentElement?.children ?? [];
for (const sibling of siblings) {
if (sibling !== group && sibling instanceof HTMLDetailsElement) {
sibling.querySelectorAll<HTMLDetailsElement>('details[open]').forEach(child => { child.open = false; });
sibling.open = false;
}
}
group.open = true;
const menu = group.querySelector<HTMLElement>('.part-command-menu');
if (focus && menu) {
const items = menuItems(menu);
items[focus === 'last' ? items.length - 1 : 0]?.focus();
}
}
function menuKeys(event: React.KeyboardEvent<HTMLElement>) {
const target = event.target as HTMLElement;
const menu = target.closest<HTMLElement>('.part-command-menu');
if (event.key === 'Tab') { closeMenu(target, true); return; }
if (event.key === 'Escape' || event.key === 'ArrowLeft' && menu?.parentElement?.classList.contains('part-submenu')) {
event.preventDefault(); event.stopPropagation();
const parent = menu?.parentElement;
if (parent instanceof HTMLDetailsElement && parent.classList.contains('part-submenu')) {
parent.querySelectorAll<HTMLDetailsElement>('details[open]').forEach(child => { child.open = false; });
parent.open = false; parent.querySelector<HTMLElement>('summary')?.focus();
} else closeMenu(target, true);
return;
}
const submenu = target.parentElement instanceof HTMLDetailsElement ? target.parentElement : undefined;
if (target.tagName === 'SUMMARY' && submenu &&
(['ArrowRight', 'Enter', ' '].includes(event.key) || !menu && ['ArrowDown', 'ArrowUp'].includes(event.key))) {
event.preventDefault(); event.stopPropagation();
openGroup(submenu, event.key === 'ArrowUp' ? 'last' : 'first'); return;
}
if (!menu) return;
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
event.preventDefault(); event.stopPropagation();
const items = menuItems(menu);
if (!items.length) return;
const index = items.indexOf(target);
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (index + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
items[next]?.focus();
}
function Action({ command, props, toolbar = false }: { command: string; props: Props; toolbar?: boolean }) {
const item = partUiActions[command];
if (!item) return null;
return <button type="button" className={toolbar ? 'part-tool-button' : undefined} data-command={command}
disabled={props.disabled(item)} aria-label={item.label} title={item.label} aria-pressed={toolbar ? props.currentTask === item.task : undefined}
onClick={event => { closeMenu(event.currentTarget, true); props.activate(item); }}>
{item.icon && <img src={`/icons/part/${item.icon}.svg`} alt="" aria-hidden="true" />}
{!item.icon && !toolbar && <span className="part-command-icon" aria-hidden="true" />}
{(!toolbar || !item.icon) && item.label}
</button>;
}
function Group({ name, commands, props, toolbar = false }: { name: string; commands: string[]; props: Props; toolbar?: boolean }) {
return <details className={toolbar ? 'part-tool-group' : 'part-submenu'} onKeyDown={toolbar ? menuKeys : undefined}>
<summary onClick={event => {
event.preventDefault();
const group = event.currentTarget.parentElement as HTMLDetailsElement;
if (group.open) group.open = false; else openGroup(group);
}}>{name}</summary><div className="part-command-menu">{commands.map(command => <Action key={command} command={command} props={props} />)}</div>
</details>;
}
export function PartWorkbenchMenu(props: Props) {
const root = useRef<HTMLDetailsElement>(null);
useEffect(() => {
const dismiss = (event: PointerEvent) => { if (root.current && !root.current.contains(event.target as Node)) closeMenu(root.current); };
document.addEventListener('pointerdown', dismiss); return () => document.removeEventListener('pointerdown', dismiss);
}, []);
return <details ref={root} className="part-menu" onKeyDown={menuKeys}><summary>Part</summary><div className="part-command-menu">
<Action command="Part_BoxSelection" props={props} /><hr />
<Group name="Primitives" commands={family('Primitives')} props={props} />
<Action command="Part_Primitives" props={props} />
<Action command="Part_Builder" props={props} /><hr />
{['Part_ShapeFromMesh', 'Part_PointsFromMesh'].map(command => <Action key={command} command={command} props={props} />)}
{['Part_MakeSolid', 'Part_ReverseShape'].map(command => <Action key={command} command={command} props={props} />)}
<Group name="Copy" commands={family('Copy')} props={props} /><hr />
{['Boolean', 'Join', 'Split', 'Compound'].map(name => <Group key={name} name={name} commands={family(name)} props={props} />)}<hr />
<Action command="Sketcher_NewSketch" props={props} />
{[...tools, 'Part_Offset', 'Part_Offset2D', 'Part_Thickness', 'Part_ProjectionOnSurface', 'Part_SectionCut'].map(command => <Action key={command} command={command} props={props} />)}<hr />
<Action command="Part_EditAttachment" props={props} /><hr />
{family('Inspection').map(command => <Action key={command} command={command} props={props} />)}
</div></details>;
}
export function PartWorkbenchViewItems(props: Props) {
return <Action command="Part_ColorPerFace" props={props} />;
}
export function PartWorkbenchToolbars(props: Props) {
useEffect(() => {
const dismiss = (event: PointerEvent) => document.querySelectorAll<HTMLDetailsElement>('.part-tool-group[open]').forEach(node => { if (!node.contains(event.target as Node)) closeMenu(node); });
document.addEventListener('pointerdown', dismiss); return () => document.removeEventListener('pointerdown', dismiss);
}, []);
return <>
<ToolbarGroup name="Solids">{family('Solids').map(command => <Action key={command} command={command} props={props} toolbar />)}</ToolbarGroup>
<ToolbarGroup name="Part Tools">{['Sketcher_NewSketch', ...tools].map(command => <Action key={command} command={command} props={props} toolbar />)}
<Group name="Offset" commands={['Part_Offset', 'Part_Offset2D']} props={props} toolbar />
{['Part_Thickness', 'Part_ProjectionOnSurface', 'Part_ColorPerFace'].map(command => <Action key={command} command={command} props={props} toolbar />)}
</ToolbarGroup>
<ToolbarGroup name="Boolean Tools"><Group name="Compound Tools" commands={family('Compound')} props={props} toolbar />
{family('Boolean').map(command => <Action key={command} command={command} props={props} toolbar />)}
<Group name="Join Shapes" commands={family('Join')} props={props} toolbar /><Group name="Split Shapes" commands={family('Split')} props={props} toolbar />
{['Part_CheckGeometry', 'Part_Defeaturing'].map(command => <Action key={command} command={command} props={props} toolbar />)}
</ToolbarGroup>
</>;
}

View File

@@ -0,0 +1,173 @@
import React, { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import type { WebModuleRouter } from "./module-router.js";
import { areas, onToolbarPreferencesChanged, restoreToolbars, saveToolbars, readToolbarVisibility, setToolbarVisibility, type Area, type Placement } from "./toolbar-preferences.js";
import { ToolbarVisibilityMenu } from "./toolbar-visibility-menu.js";
const names = ["Workbench", "Solids", "Part Tools", "Boolean Tools", "Spreadsheet tools", "3D display controls"];
const Hosts = createContext<Map<string, HTMLElement>>(new Map());
export function ToolbarGroup({ name, children }: { name: string; children: React.ReactNode }) {
const hosts = useContext(Hosts);
const host = hosts.get(name);
return host ? createPortal(children, host) : null;
}
export function PersistentToolbarWorkspace({ router, documentId, workbench, children }: {
router: WebModuleRouter; documentId: string; workbench: string; children: React.ReactNode;
}) {
const defaults = useMemo<Placement[]>(() => names.filter(name =>
name === "Workbench" || name === "3D display controls" || (workbench === "Part"
? ["Solids", "Part Tools", "Boolean Tools"].includes(name) : workbench === "Spreadsheet" && name === "Spreadsheet tools"))
.map(name => ({ name, area: "Top", breakBefore: false })), [workbench]);
const hosts = useMemo(() => new Map(names.map(name => {
const host = document.createElement("div"); host.className = "persistent-toolbar workbench-toolbar";
host.dataset.toolbarName = name; host.setAttribute("role", "toolbar"); host.setAttribute("aria-label", name);
return [name, host] as const;
})), []);
const root = useRef<HTMLDivElement>(null);
const arrangeButton = useRef<HTMLButtonElement>(null);
const editor = useRef<HTMLDivElement>(null);
const [layout, setLayout] = useState<Placement[]>(defaults);
const [visibility, setVisibility] = useState<Record<string, boolean>>({});
const [draft, setDraft] = useState<Placement[] | null>(null);
const [ready, setReady] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [restoreAttempt, setRestoreAttempt] = useState(0);
const revision = useRef(0);
useEffect(() => {
let disposed = false;
const refresh = () => {
const token = ++revision.current;
setDraft(null); setReady(false); setError("");
void Promise.all([restoreToolbars(router, documentId, workbench, defaults), readToolbarVisibility(router, documentId)]).then(([value, visible]) => {
if (!disposed && token === revision.current) { const seen = new Set<string>();
const migrated = value.map(item => ({ ...item, name: item.name === "Part tools" ? "Part Tools" : item.name }))
.filter(item => { if (seen.has(item.name) || !hosts.has(item.name)) return false; seen.add(item.name); return true; });
setLayout(migrated); setVisibility(visible); setError(""); setReady(true); }
}).catch(cause => { if (!disposed && token === revision.current) setError(String(cause)); });
};
refresh(); const unsubscribe = onToolbarPreferencesChanged(refresh);
return () => { disposed = true; ++revision.current; unsubscribe(); };
}, [router, documentId, workbench, defaults, hosts, restoreAttempt]);
useLayoutEffect(() => {
const node = root.current!;
for (const area of areas) {
const dock = node.querySelector<HTMLElement>(`[data-toolbar-area=${area}]`)!;
const oldLines = Array.from(dock.children);
let line: HTMLDivElement | undefined;
for (const item of draft ?? layout) {
const host = hosts.get(item.name);
if (item.area !== area || !host) continue;
if (!line || item.breakBefore) { line = document.createElement("div"); line.className = "toolbar-line"; dock.append(line); }
line.append(host);
}
oldLines.forEach(item => item.remove());
}
}, [hosts, layout, draft]);
useLayoutEffect(() => {
for (const [name, host] of hosts) host.hidden = visibility[name] === false;
}, [hosts, visibility]);
useEffect(() => {
const node = root.current!; const host = hosts.get("3D display controls")!;
const attach = () => {
const toolbar = node.querySelector<HTMLElement>(".scene-display-tools");
if (toolbar && toolbar.parentElement !== host) host.append(toolbar);
};
attach(); const observer = new MutationObserver(attach); observer.observe(node, { childList: true, subtree: true });
return () => observer.disconnect();
}, [hosts]);
useEffect(() => {
if (!draft) return;
editor.current?.querySelector<HTMLElement>('select:not(:disabled), button:not(:disabled)')?.focus();
}, [draft !== null]);
const cancelDraft = () => { setDraft(null); arrangeButton.current?.focus(); };
useEffect(() => {
if (!draft) return;
const cancel = (event: KeyboardEvent) => {
if (event.key !== "Escape") return;
event.preventDefault(); event.stopImmediatePropagation(); if (!busy) cancelDraft();
};
window.addEventListener("keydown", cancel, true);
return () => window.removeEventListener("keydown", cancel, true);
}, [draft, busy]);
const move = (name: string, offset: number) => setDraft(previous => {
if (!previous) return previous;
const next = previous.map(item => ({ ...item })); const index = next.findIndex(item => item.name === name);
const siblings = next.map((item, i) => item.area === next[index].area ? i : -1).filter(i => i >= 0);
const target = siblings[siblings.indexOf(index) + offset];
if (target !== undefined) [next[index], next[target]] = [next[target], next[index]];
return next;
});
const apply = async () => {
if (!draft || busy) return;
const value = draft; const token = revision.current; setBusy(true); setError("");
try {
await saveToolbars(router, documentId, workbench, value);
if (token === revision.current) { setLayout(value); setDraft(null); }
} catch (cause) { if (token === revision.current) setError(`Toolbar placement could not be saved: ${String(cause)}`); }
finally { setBusy(false); }
};
const toggleVisibility = async (name: string, visible: boolean) => {
if (!ready || busy || draft) return;
const token = revision.current; setBusy(true); setError("");
try {
await setToolbarVisibility(router, documentId, name, visible);
if (token === revision.current) setVisibility(previous => ({ ...previous, [name]: visible }));
} catch (cause) {
if (token === revision.current) setError(`Toolbar visibility could not be saved: ${String(cause)}`);
} finally { setBusy(false); }
};
return <Hosts.Provider value={hosts}><div ref={root} className="toolbar-workspace">
<style>{`
.toolbar-workspace{display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-rows:auto auto minmax(0,1fr) auto}
.toolbar-placement-controls{grid-column:1/-1;padding:4px 14px;background:#17212b}
.toolbar-dock{display:flex;flex-direction:column;align-content:flex-start;align-items:flex-start;gap:4px;min-width:0;background:#17212b}
.toolbar-dock[data-toolbar-area=Top]{grid-column:1/-1;grid-row:2;flex-wrap:wrap}
.toolbar-dock[data-toolbar-area=Bottom]{grid-column:1/-1;grid-row:4;flex-wrap:wrap}
.toolbar-dock[data-toolbar-area=Left]{grid-column:1;grid-row:3}
.toolbar-dock[data-toolbar-area=Right]{grid-column:3;grid-row:3}
.toolbar-content{grid-column:2;grid-row:3;min-width:0}
.persistent-toolbar{max-width:100%;padding:4px!important;gap:4px!important}
.persistent-toolbar:empty{display:none}
.persistent-toolbar[hidden]{display:none!important}
.toolbar-visibility-menu{min-width:210px;max-width:calc(100vw - 8px);max-height:calc(100vh - 8px);overflow:auto;padding:3px;background:#17212b;color:#e7edf3;border:1px solid #435464;box-shadow:0 4px 12px #0006}
.toolbar-visibility-menu button{display:flex;align-items:center;gap:8px;width:100%;padding:5px 8px;text-align:left;background:transparent;color:inherit;border:0;border-radius:0;font:inherit;font-size:13px}
.toolbar-visibility-menu button span{width:16px;flex-shrink:0}.toolbar-visibility-menu button:hover,.toolbar-visibility-menu button:focus-visible{background:#264b56;outline:1px solid #66c7d5;outline-offset:-1px}
.toolbar-dock .toolbar-break{flex-basis:100%;height:0}
.toolbar-line{display:flex;flex-wrap:wrap;align-items:flex-start;gap:4px}.toolbar-dock[data-toolbar-area=Left],.toolbar-dock[data-toolbar-area=Right]{flex-direction:row;max-width:35vw;overflow:auto}.toolbar-dock[data-toolbar-area=Left] .toolbar-line,.toolbar-dock[data-toolbar-area=Right] .toolbar-line{flex-direction:column;flex-wrap:nowrap}
.toolbar-dock[data-toolbar-area=Left] .persistent-toolbar,.toolbar-dock[data-toolbar-area=Right] .persistent-toolbar{width:150px;flex-direction:column;align-items:stretch}
.toolbar-dock[data-toolbar-area=Left] .toolbar-break,.toolbar-dock[data-toolbar-area=Right] .toolbar-break{width:0;height:100%;flex-basis:100%}
.persistent-toolbar:empty{display:none}
.persistent-toolbar .scene-display-tools{position:static;max-width:100%;border:0;margin:0}
.toolbar-placement-editor{display:flex;flex-wrap:wrap;gap:10px;padding:8px;border:1px solid #435464}
.toolbar-placement-editor label{display:inline-flex;gap:5px;align-items:center}.toolbar-placement-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;width:100%}
`}</style>
<div className="toolbar-placement-controls">
<ToolbarVisibilityMenu workspace={root} names={defaults.map(item => item.name)} visibility={visibility}
disabled={!ready || busy || !!draft} toggle={(name, visible) => void toggleVisibility(name, visible)} />
<button ref={arrangeButton} disabled={!ready || busy} onClick={() => setDraft(layout.map(item => ({ ...item })))}>Arrange toolbars</button>
{error && <span role="alert">{error}</span>}
{error && !ready && <button disabled={busy} onClick={() => setRestoreAttempt(attempt => attempt + 1)}>Retry toolbar preferences</button>}
{draft && <div ref={editor} className="toolbar-placement-editor" role="dialog" aria-label="Toolbar placement" onKeyDown={event => {
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); if (!busy) cancelDraft(); }
}}>
{draft.filter(item => hosts.has(item.name)).map(item => <div className="toolbar-placement-row" key={item.name}>
<strong>{item.name}</strong><label>Dock <select disabled={busy} value={item.area} onChange={event => {
const area = event.target.value as Area;
setDraft(previous => previous!.map(entry => entry.name === item.name ? { ...entry, area } : entry));
}}>{areas.map(area => <option key={area}>{area}</option>)}</select></label>
<button disabled={busy} aria-label={`Move ${item.name} earlier`} onClick={() => move(item.name, -1)}>Earlier</button>
<button disabled={busy} aria-label={`Move ${item.name} later`} onClick={() => move(item.name, 1)}>Later</button>
<label><input type="checkbox" disabled={busy} checked={item.breakBefore} onChange={event => {
const breakBefore = event.target.checked;
setDraft(previous => previous!.map(entry => entry.name === item.name ? { ...entry, breakBefore } : entry));
}} />New row / column</label>
</div>)}
<button disabled={busy} onClick={() => void apply()}>Apply placement</button>
<button disabled={busy} onClick={cancelDraft}>Cancel</button>
</div>}
</div>
{areas.map(area => <div key={area} className="toolbar-dock" data-toolbar-area={area} aria-label={`${area} toolbar dock`} />)}
<div className="toolbar-content">{children}</div>
</div></Hosts.Provider>;
}

View File

@@ -0,0 +1,121 @@
import { useEffect, useRef, useState } from "react";
import type { WebModuleRouter } from "./module-router.js";
import { createStartDesktopRequest, type StartDesktopResult } from "./start-facade.js";
import { startParameters, startDefaults, StartPreferencesEditor, FirstStartGeneralSettings } from "./start-preferences.js";
const exampleUrls = import.meta.glob("../../freecad-source/data/examples/*.{FCStd,stp}", { eager: true, query: "?url", import: "default" }) as Record<string, string>;
/** Start owns presentation only. All document operations belong to its host. */
export function StartLaunchPanel({ router, epoch, busy, onClose, onNew, onOpen, savedFiles, onOpenSaved, onSpecialized, onOpenFiles, onLaunched }: {
router: WebModuleRouter; epoch: number; busy: boolean; onClose: () => void;
onNew: () => Promise<boolean | undefined>; onOpen: () => Promise<boolean | undefined>;
onSpecialized?: (kind: "Assembly" | "Draft" | "Architecture") => Promise<boolean | undefined>;
savedFiles: string[]; onOpenSaved: (name: string) => Promise<boolean | undefined>;
onOpenFiles: (files: File[]) => Promise<boolean>;
onLaunched: (specialized: boolean) => Promise<void>;
}) {
const panel = useRef<HTMLElement>(null);
const pending = useRef(false);
const [working, setWorking] = useState(false);
const [view, setView] = useState<StartDesktopResult | null>(null);
const [error, setError] = useState("");
const [stored, setStored] = useState("");
const [retry, setRetry] = useState(0);
const [preferences, setPreferences] = useState(startDefaults);
const [ready, setReady] = useState(false);
const [generalReady, setGeneralReady] = useState(false);
const [generalBusy, setGeneralBusy] = useState(false);
const [folders, setFolders] = useState<Record<string, File[]>>({});
const [setup, setSetup] = useState(() => localStorage.getItem("freecad.start.firstStartDone") !== "1");
const [showExamples, setShowExamples] = useState(() => localStorage.getItem("freecad.start.showExamples") !== "0");
const [customFolder, setCustomFolder] = useState(() => localStorage.getItem("freecad.start.customFolder") ?? "");
const [closeStart, setCloseStart] = useState(() => localStorage.getItem("freecad.start.closeStart") === "1");
const finishSetup = async () => {
setWorking(true);
try {
await startParameters(router, { FirstStart2024: false, ShowExamples: showExamples, CustomFolder: customFolder, closeStart });
setSetup(false);
} catch (reason) { setError(String(reason)); }
finally { setWorking(false); }
};
useEffect(() => {
let disposed = false;
const read = () => { void startParameters(router).then(value => {
if (disposed) return;
setPreferences(value); setSetup(value.FirstStart2024); setShowExamples(value.ShowExamples);
setCustomFolder(value.CustomFolder); setCloseStart(value.closeStart); setReady(true);
}).catch(reason => { if (!disposed) setError(String(reason)); }); };
read(); window.addEventListener("freecad-start-preferences", read);
return () => { disposed = true; window.removeEventListener("freecad-start-preferences", read); };
}, [router, epoch, retry]);
useEffect(() => {
let disposed = false;
panel.current?.focus();
const request = createStartDesktopRequest("start-panel");
void router.request("Start", request.command, request.payload, "start-view").then(response => {
if (disposed) return;
if (response.kind === "error") throw new Error(response.error.message);
setView(response.result as StartDesktopResult); setError("");
}).catch(reason => { if (!disposed) setError(String(reason)); });
return () => { disposed = true; };
}, [router, epoch, retry]);
const launch = async (action: () => Promise<boolean | undefined>, specialized = false) => {
if (busy || pending.current) return;
pending.current = true; setWorking(true); setError("");
try {
const opened = await action();
if (opened) { await onLaunched(specialized); }
else setError("No document opened. You can try again.");
} catch (reason) { setError(String(reason)); }
finally { pending.current = false; setWorking(false); }
};
return <section ref={panel} tabIndex={-1} aria-label="Start" data-start-view={view?.view}
style={{ padding: 20, border: "1px solid #435464" }}>
<h2>Start</h2>
{!ready && <p>Loading Start preferences…</p>}
{setup ? <fieldset disabled={!ready || working}><legend>First start setup</legend>
<FirstStartGeneralSettings router={router} disabled={!ready || working} onReadyChange={setGeneralReady} onBusyChange={setGeneralBusy} />
<label><input type="checkbox" checked={showExamples} onChange={e => setShowExamples(e.target.checked)} /> Show examples</label>
<label>Custom folder <input value={customFolder} onChange={e => setCustomFolder(e.target.value)} /></label>
<label><input type="checkbox" checked={closeStart} onChange={e => setCloseStart(e.target.checked)} /> Close Start after loading</label>
<button type="button" disabled={!generalReady || generalBusy} onClick={finishSetup}>Done</button>
</fieldset> : <><p>Create a document or open a file.</p>
<fieldset disabled={busy || working || !ready || !view?.active}>
<legend>Documents</legend>
<button type="button" onClick={() => void launch(onNew)}>Empty File</button>{" "}
<button type="button" onClick={() => void launch(onOpen)}>Open File</button>
<p>FCStd, STEP, IGES, BREP and STL</p>
<div><button type="button" onClick={() => void launch(() => onSpecialized ? onSpecialized("Assembly") : Promise.reject(new Error("Assembly launch unavailable")), true)}>Assembly</button>{" "}
<button type="button" onClick={() => void launch(() => onSpecialized ? onSpecialized("Draft") : Promise.reject(new Error("Draft launch unavailable")), true)}>2D Draft</button>{" "}
<button type="button" onClick={() => void launch(() => onSpecialized ? onSpecialized("Architecture") : Promise.reject(new Error("Architecture launch unavailable")), true)}>Architecture</button></div>
<label>Saved FCStd <select value={stored} onChange={event => setStored(event.target.value)}>
<option value="">Select a saved document</option>
{savedFiles.map(name => <option key={name}>{name}</option>)}
</select></label>{" "}
<button type="button" disabled={!stored} onClick={() => void launch(() => onOpenSaved(stored))}>Open saved document</button>
{preferences.ShowExamples && <section aria-label="Examples"><h3>Examples</h3>
{Object.entries(exampleUrls).sort(([a], [b]) => a.localeCompare(b)).map(([path, url]) => {
const name = path.split("/").at(-1)!;
return <button key={path} onClick={() => void launch(async () => {
const response = await fetch(url); if (!response.ok) throw new Error(`Cannot load ${name}: ${response.status}`);
return onOpenFiles([new File([await response.blob()], name)]);
})}>{name}</button>;
})}</section>}
{preferences.CustomFolder.split(";;").filter(Boolean).map(path => <section key={path} aria-label={`Custom folder ${path}`}>
<h3>{path}</h3><label>Grant folder access<input type="file" multiple {...{ webkitdirectory: "" }} onChange={event => {
const files = Array.from(event.target.files ?? []).filter(file => file.webkitRelativePath.split("/").length === 2);
if (!files.length) { setError(`No readable files selected for ${path}. Choose the folder again.`); return; }
setFolders(current => ({ ...current, [path]: files })); event.target.value = "";
}} /></label>
{!folders[path] && <p>Folder access is required for this browser session.</p>}
{(folders[path] ?? []).filter(file => !preferences.ShowOnlyFCStd || /\.fcstd$/i.test(file.name))
.sort((a, b) => a.name.localeCompare(b.name)).map(file => <button key={file.name} onClick={() => void launch(() => onOpenFiles([file]))}>{file.name}</button>)}
</section>)}
</fieldset>
<button type="button" onClick={() => setSetup(true)}>Open first start setup</button>
<StartPreferencesEditor router={router} />
</>}
{error && <p role="alert">{error}</p>}
{(!view || !ready || error) && <button type="button" onClick={() => setRetry(value => value + 1)}>Reload Start</button>}
<button type="button" disabled={working} onClick={onClose}>Dismiss Start</button>
</section>;
}

View File

@@ -0,0 +1,115 @@
import { useEffect, useState } from "react";
import type { WebModuleRouter } from "./module-router.js";
import { requestOwnerParameters, START_PARAMETER_PATH, GENERAL_PARAMETER_PATH } from "./view-navigation-preferences.js";
import { navigationStyles, defaultNavigationStyle } from "./scene-navigation.js";
export const startDefaults = { ShowExamples: true, CustomFolder: "", ShowOnlyFCStd: false,
FirstStart2024: true, ShowOnStartup: true, closeStart: false, AutoloadModule: "" };
export type StartPreferences = typeof startDefaults;
export type StartGeneralPreferences = { Language: string; UserSchema: number; NavigationStyle: string; theme: string };
export function applyStartTheme(theme: string) {
localStorage.setItem("freecad.theme", theme);
const root = document.documentElement;
root.dataset.theme = theme;
root.style.colorScheme = theme === "system" ? "" : theme;
window.dispatchEvent(new CustomEvent("freecad-theme-changed", { detail: { theme } }));
}
export async function startParameters(router: WebModuleRouter, changes?: Partial<StartPreferences> & { LastModule?: string; Language?: string; UserSchema?: number }, general = false) {
const path = general ? GENERAL_PARAMETER_PATH : START_PARAMETER_PATH;
const request = async (args: Record<string, unknown>) => {
const reply = await requestOwnerParameters(router, "PartBox", "start-preferences", { path, ...args });
if (reply.kind !== "result") throw new Error(reply.error.message);
const result = reply.result as { parameters?: [string, string, any][]; persistenceError?: string };
if (result.persistenceError) throw new Error(`Preferences changed but could not be saved: ${result.persistenceError}`);
return Object.fromEntries((result.parameters ?? []).map(([, key, value]) => [key, value]));
};
for (const [name, value] of Object.entries(changes ?? {}))
await request({ operation: "set", name, type: typeof value === "boolean" ? "Bool" : typeof value === "number" ? "Int" : "String", value });
return { ...startDefaults, ...await request({ operation: "read" }) } as StartPreferences & { LastModule?: string };
}
export async function readStartGeneral(router: WebModuleRouter): Promise<StartGeneralPreferences> {
const values = await startParameters(router, undefined, true) as StartPreferences & { Language?: string; UserSchema?: number; NavigationStyle?: string };
const view = await requestOwnerParameters(router, "PartBox", "start-preferences", {
path: "User parameter:BaseApp/Preferences/View", operation: "read",
});
if (view.kind !== "result") throw new Error(view.error.message);
const parameters = (view.result as { parameters?: [string, string, unknown][] }).parameters ?? [];
const navigation = parameters.find(([, key]) => key === "NavigationStyle")?.[2];
return { Language: String(values.Language ?? ""), UserSchema: Number(values.UserSchema ?? 0),
NavigationStyle: typeof navigation === "string" && navigation ? navigation : defaultNavigationStyle,
theme: localStorage.getItem("freecad.theme") ?? "system" };
}
export async function saveStartGeneral(router: WebModuleRouter, value: StartGeneralPreferences) {
await startParameters(router, { Language: value.Language, UserSchema: Number(value.UserSchema) }, true);
const reply = await requestOwnerParameters(router, "PartBox", "start-preferences", {
path: "User parameter:BaseApp/Preferences/View", operation: "set", name: "NavigationStyle",
type: "String", value: value.NavigationStyle,
});
if (reply.kind !== "result") throw new Error(reply.error.message);
applyStartTheme(value.theme);
window.dispatchEvent(new Event("freecad-start-preferences"));
}
export function FirstStartGeneralSettings({ router, disabled = false, onReadyChange, onBusyChange }: {
router: WebModuleRouter; disabled?: boolean; onReadyChange?: (ready: boolean) => void; onBusyChange?: (busy: boolean) => void;
}) {
const [value, setValue] = useState<StartGeneralPreferences>({ Language: "", UserSchema: 0,
NavigationStyle: defaultNavigationStyle, theme: localStorage.getItem("freecad.theme") ?? "system" });
const [ready, setReady] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
let disposed = false;
onReadyChange?.(false);
void readStartGeneral(router).then(next => { if (!disposed) { setValue(next); setReady(true); } })
.catch(cause => { if (!disposed) { setError(String(cause)); setReady(true); } });
return () => { disposed = true; };
}, [router, onReadyChange]);
useEffect(() => { onReadyChange?.(ready); }, [ready, onReadyChange]);
const update = (next: StartGeneralPreferences) => {
setValue(next); setBusy(true); onBusyChange?.(true); setError("");
void saveStartGeneral(router, next).catch(cause => setError(String(cause))).finally(() => { setBusy(false); onBusyChange?.(false); });
};
return <fieldset disabled={disabled || busy || !ready}><legend>General settings</legend>
<label>Language <select value={value.Language} onChange={e => update({ ...value, Language: e.target.value })}>
<option value="">System default</option><option value="English">English</option><option value="中文">中文</option><option value="Deutsch">Deutsch</option><option value="Français">Français</option>
</select></label>
<label>Units <select value={String(value.UserSchema)} onChange={e => update({ ...value, UserSchema: Number(e.target.value) })}>
<option value="0">Standard</option><option value="1">SI1</option><option value="2">SI2</option><option value="5">Imperial decimal</option><option value="7">Building Euro</option>
</select></label>
<label>Navigation <select value={value.NavigationStyle} onChange={e => update({ ...value, NavigationStyle: e.target.value })}>
{navigationStyles.map(style => <option key={style.value} value={style.value}>{style.label}</option>)}
</select></label>
<label>Theme <select value={value.theme} onChange={e => update({ ...value, theme: e.target.value })}>
<option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option>
</select></label>
{error && <p role="alert">{error}</p>}
</fieldset>;
}
export function StartPreferencesEditor({ router }: { router: WebModuleRouter }) {
const [value, setValue] = useState(startDefaults);
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
useEffect(() => {
let disposed = false;
const read = () => { void startParameters(router).then(v => { if (!disposed) setValue(v); }).catch(e => { if (!disposed) setError(String(e)); }); };
read(); window.addEventListener("freecad-start-preferences", read);
return () => { disposed = true; window.removeEventListener("freecad-start-preferences", read); };
}, [router]);
const save = async () => {
setBusy(true); setError("");
try { setValue(await startParameters(router, value)); } catch (e) { setError(String(e)); }
finally { setBusy(false); }
};
return <><fieldset disabled={busy}><legend>Start preferences</legend>
{(["ShowExamples", "ShowOnlyFCStd", "ShowOnStartup", "closeStart", "FirstStart2024"] as const).map(key =>
<label key={key}><input type="checkbox" checked={value[key]} onChange={e => setValue(v => ({ ...v, [key]: e.target.checked }))} />{key}</label>)}
<label>Custom folders (separate with ;;)<input value={value.CustomFolder} onChange={e => setValue(v => ({ ...v, CustomFolder: e.target.value }))} /></label>
<label>Workbench after opening<input value={value.AutoloadModule} placeholder="Workbench name or $LastModule" onChange={e => setValue(v => ({ ...v, AutoloadModule: e.target.value }))} /></label>
<button type="button" onClick={() => void save()}>Save Start preferences</button>
{error && <p role="alert">{error}</p>}
</fieldset><FirstStartGeneralSettings router={router} /> </>;
}

View File

@@ -0,0 +1,170 @@
import type { WebModuleRouter } from "./module-router.js";
import type { ViewOwner } from "./part-view-facade.js";
import { requestOwnerParameters, saved, persist } from "./view-navigation-preferences.js";
import { createFailure, createSuccess } from "./facade-envelope.js";
import type { StandardCommandResult } from "./web-standard-command-contract.js";
export const toolbarPath = "User parameter:Tux/PersistentToolbars";
export const toolbarVisibilityPath = "User parameter:BaseApp/MainWindow/Toolbars";
export const areas = ["Top", "Left", "Right", "Bottom"] as const;
export type Area = typeof areas[number];
export type Placement = { name: string; area: Area; breakBefore: boolean };
const legacyFile = "Std_DlgParameter-PersistentToolbars.FCParam";
const file = "Std_DlgParameter-PersistentToolbars.json";
const snapshotSchema = "freecad-web/persistent-toolbars/v1";
type ToolbarSnapshot = { schema: typeof snapshotSchema; userParameters: string; enabled?: boolean; visibilityParameters?: string };
function decodeSnapshot(data: string): ToolbarSnapshot | undefined {
const text = atob(data);
if (!text.trimStart().startsWith("{")) return undefined; // Legacy User-only FCParam.
const snapshot = JSON.parse(text) as ToolbarSnapshot;
if (snapshot.schema !== snapshotSchema || typeof snapshot.userParameters !== "string"
|| snapshot.enabled !== undefined && typeof snapshot.enabled !== "boolean"
|| snapshot.visibilityParameters !== undefined && typeof snapshot.visibilityParameters !== "string")
throw new Error("Invalid persistent toolbar preference snapshot");
return snapshot;
}
function enabledParameter(root: StandardCommandResult): boolean | undefined {
const value = root.parameters?.find((entry: unknown[]) => entry[1] === "Enabled")?.[2];
if (value === undefined) return undefined;
return value !== false && value !== 0 && !["false", "0"].includes(String(value).toLowerCase());
}
const states = new WeakMap<WebModuleRouter, Map<ViewOwner, { ready: boolean; snapshot?: string; tail: Promise<unknown> }>>();
const listeners = new Set<() => void>();
let latest: string | undefined;
const channel = typeof BroadcastChannel === "undefined" ? undefined : new BroadcastChannel("freecad-toolbar-parameters");
if (channel) channel.onmessage = event => {
if (typeof event.data !== "string") return;
latest = event.data; listeners.forEach(fn => fn());
};
export function onToolbarPreferencesChanged(fn: () => void) { listeners.add(fn); return () => { listeners.delete(fn); }; }
// Persist root Enabled and User together; System/add* remain native session presets.
export function toolbarParameters(router: WebModuleRouter, owner: ViewOwner, documentId: string,
action: (request: (args: Record<string, unknown>) => Promise<StandardCommandResult>) => Promise<unknown>, changed = false) {
let owners = states.get(router);
if (!owners) { owners = new Map(); states.set(router, owners); }
let state = owners.get(owner);
if (!state) { state = { ready: false, tail: Promise.resolve() }; owners.set(owner, state); }
const current = state;
const run = async () => {
const request = async (args: Record<string, unknown>) => {
const reply = await requestOwnerParameters(router, owner, documentId, args);
if (reply.kind !== "result") throw new Error(reply.error.message);
return reply.result as StandardCommandResult;
};
const userPath = `${toolbarPath}/User`;
const capture = async () => {
const root = await request({ path: toolbarPath, operation: "read" });
const exported = await request({ path: userPath, operation: "export" });
const visibility = await request({ path: toolbarVisibilityPath, operation: "export" });
if (typeof exported.fileData !== "string" || typeof visibility.fileData !== "string")
throw new Error("Native toolbar parameter export is missing");
// Fields are ASCII (FCParam is already base64), so btoa preserves the full snapshot.
return btoa(JSON.stringify({ schema: snapshotSchema, userParameters: exported.fileData,
enabled: enabledParameter(root), visibilityParameters: visibility.fileData } satisfies ToolbarSnapshot));
};
const replace = async (data: string) => {
const snapshot = decodeSnapshot(data);
const root = await request({ path: userPath, operation: "read" });
for (const group of root.groups ?? []) for (const name of ["Saved", ...areas])
await request({ path: `${userPath}/${group}`, operation: "remove", name, type: name === "Saved" ? "Bool" : "String" });
await request({ path: userPath, operation: "import", fileData: snapshot?.userParameters ?? data });
// A legacy User-only snapshot must not reset the native root preference.
if (snapshot) await request({ path: toolbarPath,
operation: snapshot.enabled === undefined ? "remove" : "set",
name: "Enabled", type: "Bool", ...(snapshot.enabled === undefined ? {} : { value: snapshot.enabled }) });
if (snapshot?.visibilityParameters !== undefined) {
const visibility = await request({ path: toolbarVisibilityPath, operation: "read" });
for (const [type, name] of visibility.parameters ?? []) if (type === "Bool")
await request({ path: toolbarVisibilityPath, operation: "remove", name, type });
await request({ path: toolbarVisibilityPath, operation: "import", fileData: snapshot.visibilityParameters });
}
};
const data = latest ?? (!current.ready ? (await saved(file) ?? await saved(legacyFile)) : undefined);
if (data && current.snapshot !== data) {
const before = await capture();
try { await replace(data); }
catch (error) { await replace(before); throw error; }
current.snapshot = data; latest = data;
}
current.ready = true;
const before = changed ? await capture() : undefined;
try {
const result = await action(request);
if (changed) {
const exported = await capture();
if (exported === before) return result;
await persist(exported, file);
latest = exported; current.snapshot = latest;
channel?.postMessage(latest); listeners.forEach(fn => fn());
}
return result;
} catch (error) {
if (before) await replace(before);
throw error;
}
};
const pending = current.tail.then(run, run); current.tail = pending.catch(() => {}); return pending;
}
export async function restoreToolbars(router: WebModuleRouter, documentId: string, workbench: string, defaults: Placement[]): Promise<Placement[]> {
return await toolbarParameters(router, "DocumentProbe", documentId, async request => {
const root = await request({ path: toolbarPath, operation: "read" });
if (enabledParameter(root) === false) return defaults.map(item => ({ ...item }));
const read = async (kind: string) => Object.fromEntries((await request({ path: `${toolbarPath}/${kind}/${workbench}Workbench`, operation: "read" })).parameters!.map(([, key, value]) => [key, value]));
const user = await read("User");
const parameters = user.Saved ? user : await read("System");
const result: Placement[] = []; const seen = new Set<string>();
if (parameters.Saved) for (const area of areas) {
let breakBefore = false;
for (const name of String(parameters[area] ?? "").split(",")) {
if (name === "Break") { breakBefore = true; continue; }
if (name && !seen.has(name)) { result.push({ name, area, breakBefore }); seen.add(name); breakBefore = false; }
}
}
for (const item of defaults) if (!seen.has(item.name)) result.push({ ...item });
return result;
}) as Placement[];
}
export async function saveToolbars(router: WebModuleRouter, documentId: string, workbench: string, layout: Placement[]) {
return await toolbarParameters(router, "DocumentProbe", documentId, async request => {
const root = await request({ path: toolbarPath, operation: "read" });
if (enabledParameter(root) === false) return false;
const path = `${toolbarPath}/User/${workbench}Workbench`;
for (const area of areas) await request({ path, operation: "set", name: area, type: "String",
value: layout.filter(item => item.area === area).flatMap(item => item.breakBefore ? ["Break", item.name] : [item.name]).join(",") });
await request({ path, operation: "set", name: "Saved", type: "Bool", value: true });
return true;
}, true);
}
export async function readToolbarVisibility(router: WebModuleRouter, documentId: string): Promise<Record<string, boolean>> {
return await toolbarParameters(router, "DocumentProbe", documentId, async request => {
const result = await request({ path: toolbarVisibilityPath, operation: "read" });
return Object.fromEntries((result.parameters ?? []).filter(([type]) => type === "Bool")
.map(([, name, value]) => [name, value !== false && value !== 0 && !["false", "0"].includes(String(value).toLowerCase())]));
}) as Record<string, boolean>;
}
export async function setToolbarVisibility(router: WebModuleRouter, documentId: string, name: string, visible: boolean) {
// Visibility belongs to the desktop MainWindow preferences, independent of Tux's layout gate.
return toolbarParameters(router, "DocumentProbe", documentId, request =>
request({ path: toolbarVisibilityPath, operation: "set", name, type: "Bool", value: visible }), true);
}
function relatedPath(path: string, root: string) {
return path === root || path.startsWith(root + "/") || root.startsWith(path.endsWith(":") ? path : path + "/");
}
/** Native parameter-panel edits and imports use the same authority as toolbar moves. */
export function requestToolbarParameters(router: WebModuleRouter, owner: ViewOwner, documentId: string, args: Record<string, unknown>) {
const path = String(args.path ?? "User parameter:");
const mutates = ["set", "remove", "import"].includes(String(args.operation));
const user = `${toolbarPath}/User`;
const affectsUser = relatedPath(path, user) || relatedPath(path, toolbarVisibilityPath);
return toolbarParameters(router, owner, documentId, request => request(args), mutates && affectsUser)
.then(result => {
if (mutates && !affectsUser) listeners.forEach(fn => fn());
return createSuccess("toolbar-parameters", result);
}).catch(error => createFailure("toolbar-parameters", "parameter_update_failed", String(error)));
}
export function isToolbarParameterPath(path: unknown) {
const value = String(path ?? "User parameter:");
return relatedPath(value, toolbarPath) || relatedPath(value, toolbarVisibilityPath);
}

View File

@@ -0,0 +1,84 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
/** Desktop toolbar toggle actions, shared by the toolbar button and dock context menus. */
export function ToolbarVisibilityMenu({ workspace, names, visibility, disabled, toggle }: {
workspace: React.RefObject<HTMLDivElement | null>; names: string[]; visibility: Record<string, boolean>;
disabled: boolean; toggle: (name: string, visible: boolean) => void;
}) {
const [anchor, setAnchor] = useState<{ x: number; y: number } | null>(null);
const trigger = useRef<HTMLButtonElement>(null);
const menu = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
const close = (restore = true) => {
setAnchor(null);
if (restore) {
const previous = previousFocus.current;
if (previous?.isConnected && previous.getClientRects().length) previous.focus();
else trigger.current?.focus();
}
};
const open = (x: number, y: number) => {
previousFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : trigger.current;
setAnchor({ x, y });
};
useEffect(() => {
const root = workspace.current;
if (!root) return;
const context = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.toolbar-dock, .toolbar-placement-controls')) return;
event.preventDefault();
if (disabled) return;
const rect = target.getBoundingClientRect();
open(event.clientX || rect.left, event.clientY || rect.bottom);
};
root.addEventListener('contextmenu', context);
return () => root.removeEventListener('contextmenu', context);
}, [workspace, disabled]);
useLayoutEffect(() => {
if (!anchor || !menu.current) return;
const element = menu.current;
element.style.left = `${Math.max(4, Math.min(anchor.x, window.innerWidth - element.offsetWidth - 4))}px`;
element.style.top = `${Math.max(4, Math.min(anchor.y, window.innerHeight - element.offsetHeight - 4))}px`;
element.querySelector<HTMLButtonElement>('button')?.focus();
}, [anchor]);
useEffect(() => {
if (!anchor) return;
const dismiss = (event: PointerEvent) => {
if (!menu.current?.contains(event.target as Node) && !trigger.current?.contains(event.target as Node)) close(false);
};
const resize = () => close();
document.addEventListener('pointerdown', dismiss);
window.addEventListener('resize', resize);
return () => { document.removeEventListener('pointerdown', dismiss); window.removeEventListener('resize', resize); };
}, [anchor]);
useEffect(() => { if (disabled) setAnchor(null); }, [disabled]);
return <>
<button ref={trigger} type="button" disabled={disabled} aria-haspopup="menu" aria-expanded={!!anchor}
onClick={() => {
if (anchor) { close(); return; }
const rect = trigger.current!.getBoundingClientRect(); open(rect.left, rect.bottom);
}}>Toolbars</button>
{anchor && createPortal(<div ref={menu} role="menu" aria-label="Toolbars" className="toolbar-visibility-menu"
style={{ position: 'fixed', left: anchor.x, top: anchor.y, zIndex: 1000 }} onKeyDown={event => {
if (event.key === 'Escape' || event.key === 'Tab') {
if (event.key === 'Escape') event.preventDefault();
event.stopPropagation(); close(); return;
}
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
event.preventDefault(); event.stopPropagation();
const items = [...event.currentTarget.querySelectorAll<HTMLButtonElement>('button')];
if (!items.length) return;
const index = items.indexOf(document.activeElement as HTMLButtonElement);
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1
: (index + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
items[next]?.focus();
}}>
{names.map(name => <button key={name} type="button" role="menuitemcheckbox" aria-checked={visibility[name] !== false}
title="Toggles this toolbar" onClick={() => { close(); toggle(name, visibility[name] === false); }}>
<span aria-hidden="true">{visibility[name] !== false ? '✓' : ''}</span>{name}
</button>)}
</div>, document.body)}
</>;
}