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
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:
90
web/public/icons/part/Part_Attachment.svg
Normal file
90
web/public/icons/part/Part_Attachment.svg
Normal 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 |
140
web/public/icons/part/Part_BoxSelection.svg
Normal file
140
web/public/icons/part/Part_BoxSelection.svg
Normal 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 |
281
web/public/icons/part/Part_PointsFromMesh.svg
Normal file
281
web/public/icons/part/Part_PointsFromMesh.svg
Normal 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 |
107
web/public/icons/part/Part_Shape_from_Mesh.svg
Normal file
107
web/public/icons/part/Part_Shape_from_Mesh.svg
Normal 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 |
81
web/public/icons/part/Sketcher_NewSketch.svg
Normal file
81
web/public/icons/part/Sketcher_NewSketch.svg
Normal 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 |
204
web/src/part-workbench-ui.tsx
Normal file
204
web/src/part-workbench-ui.tsx
Normal 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>
|
||||
</>;
|
||||
}
|
||||
173
web/src/persistent-toolbar-workspace.tsx
Normal file
173
web/src/persistent-toolbar-workspace.tsx
Normal 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>;
|
||||
}
|
||||
121
web/src/start-launch-panel.tsx
Normal file
121
web/src/start-launch-panel.tsx
Normal 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>;
|
||||
}
|
||||
115
web/src/start-preferences.tsx
Normal file
115
web/src/start-preferences.tsx
Normal 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} /> </>;
|
||||
}
|
||||
170
web/src/toolbar-preferences.ts
Normal file
170
web/src/toolbar-preferences.ts
Normal 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);
|
||||
}
|
||||
84
web/src/toolbar-visibility-menu.tsx
Normal file
84
web/src/toolbar-visibility-menu.tsx
Normal 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)}
|
||||
</>;
|
||||
}
|
||||
Reference in New Issue
Block a user