Compare commits

...

8 commits

61 changed files with 1466 additions and 141 deletions

5
.gitignore vendored
View file

@ -10,4 +10,7 @@
/.cache
# Gdb
/.gdb_history
/.gdb_history
# Excluded assets
/assets/excluded

View file

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 715 B

View file

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 354 B

View file

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

View file

Before

Width:  |  Height:  |  Size: 688 B

After

Width:  |  Height:  |  Size: 688 B

View file

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 774 B

View file

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 868 B

View file

Before

Width:  |  Height:  |  Size: 836 B

After

Width:  |  Height:  |  Size: 836 B

View file

Before

Width:  |  Height:  |  Size: 865 B

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

View file

@ -0,0 +1,8 @@
[Icon Theme]
Name=editor-dark
Comment=icon theme (dark)
[actions/48]
Size=48
Context=Actions
Type=Fixed

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

View file

@ -0,0 +1,8 @@
[Icon Theme]
Name=editor
Comment=icon theme
[actions/48]
Size=48
Context=Actions
Type=Fixed

View file

@ -39,8 +39,9 @@ const int FaceFront = 5;
// I/O
in vec3 vPos;
in vec3 lPos;
in vec3 vNormal;
in vec2 vTexCoords;
in vec3 lNormal;
flat in int vSurfaceZ;
out vec4 FragColor;
@ -54,12 +55,13 @@ uniform DirLight sunLight;
uniform Material material;
uniform sampler2DArray studs;
uniform float transparency;
uniform vec3 texScale;
// Functions
vec3 calculateDirectionalLight(DirLight light);
vec3 calculatePointLight(PointLight light);
mat3 lookAlong(vec3 pos, vec3 forward, vec3 up);
// Main
@ -71,11 +73,28 @@ void main() {
for (int i = 0; i < numPointLights; i++) {
result += calculatePointLight(pointLights[i]);
}
vec3 otherVec = abs(dot(lNormal, vec3(0, 1, 0))) > 0.99 ? vec3(0, 0, 1)
: abs(dot(lNormal, vec3(0, 0, 1))) > 0.99 ? vec3(1, 0, 0)
: vec3(0, 1, 0);
// We use abs(lNormal) so opposing sides "cut" from the same side
mat3 transform = transpose(inverse(lookAlong(vec3(0, 0, 0), abs(lNormal), otherVec)));
vec4 studPx = texture(studs, vec3(vTexCoords, vSurfaceZ));
vec2 texCoords = vec2((transform * lPos) * (transform * texScale) / 2) - vec2(mod((transform * texScale) / 4, 1));
vec4 studPx = texture(studs, vec3(texCoords, vSurfaceZ));
FragColor = vec4(mix(result, vec3(studPx), studPx.w), 1) * (1-transparency);
}
mat3 lookAlong(vec3 pos, vec3 forward, vec3 up) {
vec3 f = normalize(forward); // Forward/Look
vec3 u = normalize(up); // Up
vec3 s = normalize(cross(f, u)); // Right
u = normalize(cross(s, f));
return mat3(s, u, f);
}
vec3 calculateDirectionalLight(DirLight light) {
// Calculate diffuse
vec3 norm = normalize(vNormal);

View file

@ -18,7 +18,9 @@ const int SurfaceInlets = 4;
const int SurfaceUniversal = 5;
out vec3 vPos;
out vec3 lPos;
out vec3 vNormal;
out vec3 lNormal;
out vec2 vTexCoords;
flat out int vSurfaceZ;
@ -33,30 +35,16 @@ void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
vPos = vec3(model * vec4(aPos, 1.0));
lPos = aPos;
vNormal = normalMatrix * aNormal;
lNormal = aNormal;
int vFace = aNormal == vec3(0,1,0) ? FaceTop :
aNormal == vec3(0, -1, 0) ? FaceBottom :
aNormal == vec3(1, 0, 0) ? FaceRight :
aNormal == vec3(-1, 0, 0) ? FaceLeft :
aNormal == vec3(0, 0, 1) ? FaceFront :
aNormal == vec3(0, 0, -1) ? FaceBack : -1;
aNormal == vec3(0, 0, -1) ? FaceFront :
aNormal == vec3(0, 0, 1) ? FaceBack : -1;
vSurfaceZ = surfaces[vFace];
// if (surfaces[vFace] > SurfaceUniversal) vSurfaceZ = 0;
switch (vFace) {
case FaceTop:
case FaceBottom:
// vTexCoords = aTexCoords * vec2(texScale.x / 2, fract(surfaceOffset + texScale.z / 12));
vTexCoords = aTexCoords * vec2(texScale.x, texScale.z) / 2;
break;
case FaceLeft:
case FaceRight:
vTexCoords = aTexCoords * vec2(texScale.y, texScale.z) / 2;
break;
case FaceFront:
case FaceBack:
vTexCoords = aTexCoords * vec2(texScale.x, texScale.y) / 2;
break;
};
}

92
assets/src/glue.svg Normal file
View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
inkscape:export-filename="../icons/editor/weld.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
sodipodi:docname="glue.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="32"
inkscape:cx="2.4375"
inkscape:cy="10.25"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<inkscape:grid
id="grid1"
units="px"
originx="0.5"
originy="0.5"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g3">
<rect
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="10"
height="10"
x="2.5"
y="2.5" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,2.5 2,2"
id="path1"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,12.5 2,2"
id="path2"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.5,12.5 2,2"
id="path3"
sodipodi:nodetypes="cc" />
</g>
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 4.8592944,7.0236254 c -1.213294,1.0424696 -1,1.7243022 -0.234375,1.7243022 -0.017005,3.5315634 2.2303514,1.8633974 3.234375,1 2.7742076,1.7680844 3.5903366,0.4332984 2,-1.248699 2.4102876,-1.0931787 0.4460256,-4.39491 -1,-2.751301 C 8.5061317,2.1238839 6.180487,5.3900806 6.5155444,4.5916776 4.7251975,3.6478829 4.0183403,5.4550835 4.8592944,7.0236254 Z"
id="path18"
sodipodi:nodetypes="ccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

103
assets/src/inlets.svg Normal file
View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
inkscape:export-filename="../icons/editor/studs.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
sodipodi:docname="inlets.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="22.627417"
inkscape:cx="9.3691648"
inkscape:cy="12.772116"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid1"
units="px"
originx="0.5"
originy="0.5"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g3">
<rect
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="10"
height="10"
x="2.5"
y="2.5" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,2.5 2,2"
id="path1"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,12.5 2,2"
id="path2"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.5,12.5 2,2"
id="path3"
sodipodi:nodetypes="cc" />
</g>
<g
id="g3-3"
transform="matrix(-0.5,0,0,-0.5,11.75,11.75)"
style="stroke-width:2;stroke-dasharray:none" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 10.5,4.5 h -6 v 6 h 6 z"
id="path4"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 6.5,6.5 -2,-2"
id="path5" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 6.5,10.5 v -4 h 4"
id="path6" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

121
assets/src/localspace.svg Normal file
View file

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16mm"
height="16mm"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
sodipodi:docname="localspace.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="5.701459"
inkscape:cx="-3.3324803"
inkscape:cy="14.908465"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid3"
units="mm"
originx="0"
originy="0"
spacingx="0.99999994"
spacingy="0.99999994"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="false"
visible="true" />
<inkscape:grid
id="grid8"
units="mm"
originx="0"
originy="0"
spacingx="0.99999997"
spacingy="0.99999997"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g8"
style="fill:none;fill-opacity:1;stroke:#3daee9;stroke-opacity:1"
transform="translate(0.556875,-0.37125)">
<path
style="fill:none;fill-opacity:1;stroke:#3daee9;stroke-width:0.799999;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="M 1.9999999,11 5.9999998,12 8.788906,10.153086 4.7889062,9.1530855 Z"
id="path1"
sodipodi:nodetypes="ccccc" />
<path
style="fill:none;fill-opacity:1;stroke:#3daee9;stroke-width:0.799999;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="m 1.9999999,11 v 3 l 3.9999999,1 v -3"
id="path2"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;fill-opacity:1;stroke:#3daee9;stroke-width:0.799999;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="M 5.9999998,15 8.788906,13.153086 v -3"
id="path3"
sodipodi:nodetypes="ccc" />
</g>
<g
id="g7"
transform="translate(-0.39999908,0.10000091)"
style="stroke:#232629;stroke-opacity:1">
<path
id="path4"
style="fill:none;stroke:#232629;stroke-width:0.799999;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="m 15,3.9999999 c 0,1.568079 -1.343147,2.9999997 -3,2.9999999 -1.656854,-1e-7 -3.0000007,-1.4319208 -3.0000007,-2.9999999 0,-1.5680791 1.3431467,-2.99999993 3.0000007,-2.99999993 1.656853,1.3e-7 3,1.43192093 3,2.99999993 z"
sodipodi:nodetypes="sssss" />
<path
style="fill:none;stroke:#232629;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 8.9999994,3.9999999 H 15"
id="path5" />
<path
style="fill:none;stroke:#232629;stroke-width:0.420712;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 11.5,1 c 0,0 -0.709961,0.5 -0.709961,3.0000001 0,3.0000001 0.709961,2.7575037 0.709961,2.7575037"
id="path6"
sodipodi:nodetypes="csc" />
<path
style="fill:none;stroke:#232629;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,1 c 0,0 0.738965,0.4999998 0.738965,2.9999999 0,2.5000001 -0.738966,2.7999998 -0.738966,2.7999998"
id="path7"
sodipodi:nodetypes="csc" />
</g>
<path
style="fill:none;fill-opacity:1;stroke:#3daee9;stroke-width:0.8;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1.8199217,1.5702338 0.016407,3.8000003 h 2.3695758"
id="path8"
sodipodi:nodetypes="ccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

86
assets/src/smooth.svg Normal file
View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
inkscape:export-filename="../icons/editor/studs.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
sodipodi:docname="smooth.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="32.252323"
inkscape:cx="12.650252"
inkscape:cy="8.4335011"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid1"
units="px"
originx="0.5"
originy="0.5"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g3">
<rect
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="10"
height="10"
x="2.5"
y="2.5" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,2.5 2,2"
id="path1"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,12.5 2,2"
id="path2"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.5,12.5 2,2"
id="path3"
sodipodi:nodetypes="cc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

113
assets/src/studs.svg Normal file
View file

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
inkscape:export-filename="studs.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
sodipodi:docname="studs.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="32.252323"
inkscape:cx="12.650252"
inkscape:cy="8.4024956"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid1"
units="px"
originx="0.5"
originy="0.5"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g3">
<rect
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="10"
height="10"
x="2.5"
y="2.5" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,2.5 2,2"
id="path1"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,12.5 2,2"
id="path2"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.5,12.5 2,2"
id="path3"
sodipodi:nodetypes="cc" />
</g>
<g
id="g3-3"
transform="matrix(0.5,0,0,0.5,3.25,3.25)"
style="stroke-width:2;stroke-dasharray:none">
<rect
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1-5"
width="10"
height="10"
x="2.5"
y="2.5" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,2.5 2,2"
id="path1-6"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,12.5 2,2"
id="path2-2"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.5,12.5 2,2"
id="path3-9"
sodipodi:nodetypes="cc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

154
assets/src/universal.svg Normal file
View file

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
inkscape:export-filename="../icons/editor/studs.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
sodipodi:docname="universal.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="45.254834"
inkscape:cx="9.7448153"
inkscape:cy="5.3033009"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<inkscape:grid
id="grid1"
units="px"
originx="1"
originy="1"
spacingx="0.25"
spacingy="0.25"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
<sodipodi:guide
position="3.5,5.5"
orientation="-0.70710678,-0.70710678"
id="guide13"
inkscape:locked="false" />
<sodipodi:guide
position="3.5,4.5"
orientation="0.70710678,-0.70710678"
id="guide14"
inkscape:locked="false" />
<sodipodi:guide
position="11,12"
orientation="-0.70710678,-0.70710678"
id="guide15"
inkscape:locked="false" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g3">
<rect
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="10"
height="10"
x="2.5"
y="2.5" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,2.5 2,2"
id="path1"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,12.5 2,2"
id="path2"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.5,12.5 2,2"
id="path3"
sodipodi:nodetypes="cc" />
</g>
<g
id="g3-3"
transform="matrix(0.5,0,0,0.5,3.25,3.25)"
style="display:inline;stroke-width:2;stroke-dasharray:none">
<path
style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 12.5,12.5 h -8 z"
id="rect1-5"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 13.5,3.5 1,1"
id="path1-6"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,12.5 2,2"
id="path2-2"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 3.5,13.5 1,1"
id="path3-9"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,12.5 v -8"
id="path9" />
</g>
<g
id="g6"
style="display:inline">
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 10.5,4.5 h -6 v 6"
id="path4"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 6.5,6.5 -2,-2"
id="path5" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 6.5,8.5 v -2 h 2"
id="path6"
sodipodi:nodetypes="ccc" />
</g>
<path
style="display:inline;fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 10.863301,4.1698442 4.25,10.75"
id="path10"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

102
assets/src/weld.svg Normal file
View file

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
inkscape:export-filename="../icons/editor/studs.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
sodipodi:docname="weld.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="45.611673"
inkscape:cx="6.6211122"
inkscape:cy="6.40187"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid1"
units="px"
originx="0.5"
originy="0.5"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g3">
<rect
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect1"
width="10"
height="10"
x="2.5"
y="2.5" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,2.5 2,2"
id="path1"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,12.5 2,2"
id="path2"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 2.5,12.5 2,2"
id="path3"
sodipodi:nodetypes="cc" />
</g>
<path
style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 4.5,4.5 2,2"
id="path4" />
<path
style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 8.5,6.5 2,-2"
id="path5" />
<path
style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 8.5,8.5 2,2"
id="path6" />
<path
style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round"
d="m 6.5,8.5 -2,2"
id="path7" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

121
assets/src/worldspace.svg Normal file
View file

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16mm"
height="16mm"
viewBox="0 0 16 16"
version="1.1"
id="svg1"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
sodipodi:docname="worldspace.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="5.701459"
inkscape:cx="-3.3324803"
inkscape:cy="14.908465"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid3"
units="mm"
originx="0"
originy="0"
spacingx="0.99999994"
spacingy="0.99999994"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="false"
visible="true" />
<inkscape:grid
id="grid8"
units="mm"
originx="0"
originy="0"
spacingx="0.99999997"
spacingy="0.99999997"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g8"
style="fill:none;fill-opacity:1;stroke:#232629;stroke-opacity:1"
transform="translate(0.556875,-0.37125)">
<path
style="fill:none;fill-opacity:1;stroke:#232629;stroke-width:0.799999;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="M 1.9999999,11 5.9999998,12 8.788906,10.153086 4.7889062,9.1530855 Z"
id="path1"
sodipodi:nodetypes="ccccc" />
<path
style="fill:none;fill-opacity:1;stroke:#232629;stroke-width:0.799999;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="m 1.9999999,11 v 3 l 3.9999999,1 v -3"
id="path2"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;fill-opacity:1;stroke:#232629;stroke-width:0.799999;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="M 5.9999998,15 8.788906,13.153086 v -3"
id="path3"
sodipodi:nodetypes="ccc" />
</g>
<g
id="g7"
transform="translate(-0.39999908,0.10000091)"
style="stroke:#3daee9;stroke-opacity:1">
<path
id="path4"
style="fill:none;stroke:#3daee9;stroke-width:0.799999;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="m 15,3.9999999 c 0,1.568079 -1.343147,2.9999997 -3,2.9999999 -1.656854,-1e-7 -3.0000007,-1.4319208 -3.0000007,-2.9999999 0,-1.5680791 1.3431467,-2.99999993 3.0000007,-2.99999993 1.656853,1.3e-7 3,1.43192093 3,2.99999993 z"
sodipodi:nodetypes="sssss" />
<path
style="fill:none;stroke:#3daee9;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 8.9999994,3.9999999 H 15"
id="path5" />
<path
style="fill:none;stroke:#3daee9;stroke-width:0.420712;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 11.5,1 c 0,0 -0.709961,0.5 -0.709961,3.0000001 0,3.0000001 0.709961,2.7575037 0.709961,2.7575037"
id="path6"
sodipodi:nodetypes="csc" />
<path
style="fill:none;stroke:#3daee9;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 12.5,1 c 0,0 0.738965,0.4999998 0.738965,2.9999999 0,2.5000001 -0.738966,2.7999998 -0.738966,2.7999998"
id="path7"
sodipodi:nodetypes="csc" />
</g>
<path
style="display:inline;fill:none;fill-opacity:1;stroke:#3daee9;stroke-width:0.799999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1.4102681,2.1569419 0.9999999,2.9999999 1,-1 1,1 0.9999999,-2.9999999"
id="path9"
sodipodi:nodetypes="ccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -45,12 +45,12 @@ int main() {
glfwMakeContextCurrent(window);
glewInit();
dataModel->Init();
gDataModel->Init();
simulationInit();
renderInit(window, 1200, 900);
// Baseplate
workspace()->AddChild(Part::New({
gWorkspace()->AddChild(Part::New({
.position = glm::vec3(0, -5, 0),
.rotation = glm::vec3(0),
.size = glm::vec3(512, 1.2, 512),
@ -58,14 +58,14 @@ int main() {
.anchored = true,
}));
workspace()->AddChild(lastPart = Part::New({
gWorkspace()->AddChild(lastPart = Part::New({
.position = glm::vec3(0),
.rotation = glm::vec3(0),
.size = glm::vec3(4, 1.2, 2),
.color = glm::vec3(0.639216f, 0.635294f, 0.647059f),
}));
for (InstanceRef inst : workspace()->GetChildren()) {
for (InstanceRef inst : gWorkspace()->GetChildren()) {
if (inst->GetClass()->className != "Part") continue;
std::shared_ptr<Part> part = std::dynamic_pointer_cast<Part>(inst);
syncPartPhysics(part);
@ -158,7 +158,7 @@ void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (key == GLFW_KEY_F && action == GLFW_PRESS) {
workspace()->AddChild(lastPart = Part::New({
gWorkspace()->AddChild(lastPart = Part::New({
.position = camera.cameraPos + camera.cameraFront * glm::vec3(3),
.rotation = glm::vec3(0),
.size = glm::vec3(1, 1, 1),

View file

@ -5,7 +5,7 @@
Camera camera(glm::vec3(0.0, 0.0, 3.0));
//std::vector<Part> parts;
std::shared_ptr<DataModel> dataModel = DataModel::New();
std::shared_ptr<DataModel> gDataModel = DataModel::New();
std::optional<HierarchyPreUpdateHandler> hierarchyPreUpdateHandler;
std::optional<HierarchyPostUpdateHandler> hierarchyPostUpdateHandler;
std::shared_ptr<Handles> editorToolHandles = Handles::New();

View file

@ -15,8 +15,8 @@ typedef std::function<void(std::vector<InstanceRefWeak> oldSelection, std::vecto
// TEMPORARY COMMON DATA FOR VARIOUS INTERNAL COMPONENTS
extern Camera camera;
extern std::shared_ptr<DataModel> dataModel;
inline std::shared_ptr<Workspace> workspace() { return std::dynamic_pointer_cast<Workspace>(dataModel->services["Workspace"]); }
extern std::shared_ptr<DataModel> gDataModel;
inline std::shared_ptr<Workspace> gWorkspace() { return std::dynamic_pointer_cast<Workspace>(gDataModel->services["Workspace"]); }
extern std::optional<HierarchyPreUpdateHandler> hierarchyPreUpdateHandler;
extern std::optional<HierarchyPostUpdateHandler> hierarchyPostUpdateHandler;
extern std::shared_ptr<Handles> editorToolHandles;

View file

@ -72,12 +72,51 @@ bool Instance::SetParent(std::optional<std::shared_ptr<Instance>> newParent) {
return true;
}
std::optional<std::shared_ptr<DataModel>> Instance::dataModel() {
// TODO: This algorithm will defer calculations to every time the root data model
// is accessed from any instance. This is inefficient as this can happen many times
// a tick. A better option is to cache these values and only update them if the ancestry
// changes, as that happens way less often.
std::optional<std::shared_ptr<Instance>> currentParent = GetParent();
while (currentParent) {
if (currentParent.value()->GetClass() == &DataModel::TYPE)
return std::dynamic_pointer_cast<DataModel>(currentParent.value());
currentParent = currentParent.value()->GetParent();
}
return std::nullopt;
}
std::optional<std::shared_ptr<Workspace>> Instance::workspace() {
// See comment in above function
std::optional<std::shared_ptr<Instance>> currentParent = GetParent();
while (currentParent) {
if (currentParent.value()->GetClass() == &DataModel::TYPE)
return std::dynamic_pointer_cast<Workspace>(currentParent.value());
currentParent = currentParent.value()->GetParent();
}
return std::nullopt;
}
std::optional<std::shared_ptr<Instance>> Instance::GetParent() {
if (!parent.has_value()) return std::nullopt;
if (parent.value().expired()) return std::nullopt;
return parent.value().lock();
}
static std::shared_ptr<Instance> DUMMY_INSTANCE;
DescendantsIterator Instance::GetDescendantsStart() {
return DescendantsIterator(GetChildren().size() > 0 ? GetChildren()[0] : DUMMY_INSTANCE);
}
DescendantsIterator Instance::GetDescendantsEnd() {
return DescendantsIterator(DUMMY_INSTANCE);
}
bool Instance::IsParentLocked() {
return this->parentLocked;
}
@ -201,4 +240,49 @@ InstanceRef Instance::Deserialize(pugi::xml_node* node) {
}
return object;
}
// DescendantsIterator
DescendantsIterator::DescendantsIterator(std::shared_ptr<Instance> current) : current(current), root(current == DUMMY_INSTANCE ? DUMMY_INSTANCE : current->GetParent()), siblingIndex { 0 } { }
DescendantsIterator::self_type DescendantsIterator::operator++(int _) {
// If the current item is dummy, an error has occurred, this is not supposed to happen.
if (current == DUMMY_INSTANCE) {
Logger::fatalError("Attempt to increment a descendant iterator past its end\n");
panic();
}
// If the current item has children, enter it
if (current->GetChildren().size() > 0) {
siblingIndex.push_back(0);
current = current->GetChildren()[0];
return *this;
}
// Otherwise, we move to the next sibling, if applicable.
// But not if one up is null or the root element
if (!current->GetParent() || current == root) {
current = DUMMY_INSTANCE;
return *this;
}
// If we've hit the end of this item's children, move one up
while (current->GetParent() && current->GetParent().value()->GetChildren().size() <= (siblingIndex.back() + 1)) {
siblingIndex.pop_back();
current = current->GetParent().value();
// But not if one up is null or the root element
if (!current->GetParent() || current == root) {
current = DUMMY_INSTANCE;
return *this;
}
}
// Now move to the next sibling
siblingIndex.back()++;
current = current->GetParent().value()->GetChildren()[siblingIndex.back()];
return *this;
}

View file

@ -1,5 +1,6 @@
#pragma once
#include <iterator>
#include <vector>
#include <memory>
#include <optional>
@ -19,6 +20,9 @@
class Instance;
typedef std::shared_ptr<Instance>(*InstanceConstructor)();
class DataModel;
class Workspace;
// Struct describing information about an instance
struct InstanceType {
const InstanceType* super; // May be null
@ -27,6 +31,8 @@ struct InstanceType {
std::string explorerIcon = "";
};
class DescendantsIterator;
// Base class for all instances in the data model
// Note: enable_shared_from_this HAS to be public or else its field will not be populated
// Maybe this could be replaced with a friendship? But that seems unnecessary.
@ -48,6 +54,13 @@ protected:
virtual void OnParentUpdated(std::optional<std::shared_ptr<Instance>> oldParent, std::optional<std::shared_ptr<Instance>> newParent);
// The root data model this object is a descendant of
std::optional<std::shared_ptr<DataModel>> dataModel();
// The root workspace this object is a descendant of
// NOTE: This value is not necessarily present if dataModel is present
// Objects under services other than workspace will NOT have this field set
std::optional<std::shared_ptr<Workspace>> workspace();
template <typename T> inline std::shared_ptr<T> shared() { return std::dynamic_pointer_cast<T>(this->shared_from_this()); }
public:
const static InstanceType TYPE;
@ -59,7 +72,9 @@ public:
std::optional<std::shared_ptr<Instance>> GetParent();
bool IsParentLocked();
inline const std::vector<std::shared_ptr<Instance>> GetChildren() { return children; }
DescendantsIterator GetDescendantsStart();
DescendantsIterator GetDescendantsEnd();
// Utility functions
inline void AddChild(std::shared_ptr<Instance> object) { object->SetParent(this->shared_from_this()); }
@ -77,4 +92,28 @@ public:
};
typedef std::shared_ptr<Instance> InstanceRef;
typedef std::weak_ptr<Instance> InstanceRefWeak;
typedef std::weak_ptr<Instance> InstanceRefWeak;
// https://gist.github.com/jeetsukumaran/307264
class DescendantsIterator {
public:
typedef DescendantsIterator self_type;
typedef std::shared_ptr<Instance> value_type;
typedef std::shared_ptr<Instance>& reference;
typedef std::shared_ptr<Instance> pointer;
typedef std::forward_iterator_tag iterator_category;
typedef int difference_type;
DescendantsIterator(std::shared_ptr<Instance> current);
inline self_type operator++() { self_type i = *this; ++*this; return i; }
inline std::shared_ptr<Instance> operator*() { return current; }
inline std::shared_ptr<Instance> operator->() { return current; }
inline bool operator==(const self_type& rhs) { return current == rhs.current; }
inline bool operator!=(const self_type& rhs) { return current != rhs.current; }
self_type operator++(int _);
private:
std::optional<std::shared_ptr<Instance>> root;
std::shared_ptr<Instance> current;
std::vector<int> siblingIndex;
};

View file

@ -8,6 +8,7 @@
#include "datatypes/cframe.h"
#include "datatypes/color3.h"
#include "datatypes/vector.h"
#include "rendering/surface.h"
#include <reactphysics3d/reactphysics3d.h>
namespace rp = reactphysics3d;
@ -37,6 +38,13 @@ public:
bool anchored = false;
rp::RigidBody* rigidBody = nullptr;
SurfaceType topSurface = SurfaceType::SurfaceStuds;
SurfaceType bottomSurface = SurfaceType::SurfaceInlets;
SurfaceType leftSurface = SurfaceType::SurfaceSmooth;
SurfaceType rightSurface = SurfaceType::SurfaceSmooth;
SurfaceType frontSurface = SurfaceType::SurfaceSmooth;
SurfaceType backSurface = SurfaceType::SurfaceSmooth;
Part();
Part(PartConstructParams params);

View file

@ -77,7 +77,8 @@ void physicsStep(float deltaTime) {
// Naive implementation. Parts are only considered so if they are just under Workspace
// TODO: Add list of tracked parts in workspace based on their ancestry using inWorkspace property of Instance
for (InstanceRef obj : workspace()->GetChildren()) {
for (auto it = gWorkspace()->GetDescendantsStart(); it != gWorkspace()->GetDescendantsEnd(); it++) {
InstanceRef obj = *it;
if (obj->GetClass()->className != "Part") continue; // TODO: Replace this with a .IsA call instead of comparing the class name directly
std::shared_ptr<Part> part = std::dynamic_pointer_cast<Part>(obj);
const rp::Transform& transform = part->rigidBody->getTransform();

View file

@ -109,9 +109,6 @@ void renderParts() {
// });
studsTexture->activate(0);
shader->set("studs", 0);
// shader->set("surfaces[1]", SurfaceStuds);
shader->set("surfaces[1]", SurfaceStuds);
shader->set("surfaces[4]", SurfaceInlets);
// Pre-calculate the normal matrix for the shader
@ -120,7 +117,8 @@ void renderParts() {
// Sort by nearest
std::map<float, std::shared_ptr<Part>> sorted;
for (InstanceRef inst : workspace()->GetChildren()) {
for (auto it = gWorkspace()->GetDescendantsStart(); it != gWorkspace()->GetDescendantsEnd(); it++) {
InstanceRef inst = *it;
if (inst->GetClass()->className != "Part") continue;
std::shared_ptr<Part> part = std::dynamic_pointer_cast<Part>(inst);
if (part->transparency > 0.00001) {
@ -141,6 +139,13 @@ void renderParts() {
shader->set("texScale", part->size);
shader->set("transparency", part->transparency);
shader->set("surfaces[" + std::to_string(NormalId::Right) + "]", part->rightSurface);
shader->set("surfaces[" + std::to_string(NormalId::Top) + "]", part->topSurface);
shader->set("surfaces[" + std::to_string(NormalId::Back) + "]", part->backSurface);
shader->set("surfaces[" + std::to_string(NormalId::Left) + "]", part->leftSurface);
shader->set("surfaces[" + std::to_string(NormalId::Bottom) + "]", part->bottomSurface);
shader->set("surfaces[" + std::to_string(NormalId::Front) + "]", part->frontSurface);
CUBE_MESH->bind();
glDrawArrays(GL_TRIANGLES, 0, CUBE_MESH->vertexCount);
}
@ -164,6 +169,13 @@ void renderParts() {
shader->set("texScale", part->size);
shader->set("transparency", part->transparency);
shader->set("surfaces[" + std::to_string(NormalId::Right) + "]", part->rightSurface);
shader->set("surfaces[" + std::to_string(NormalId::Top) + "]", part->topSurface);
shader->set("surfaces[" + std::to_string(NormalId::Back) + "]", part->backSurface);
shader->set("surfaces[" + std::to_string(NormalId::Left) + "]", part->leftSurface);
shader->set("surfaces[" + std::to_string(NormalId::Bottom) + "]", part->bottomSurface);
shader->set("surfaces[" + std::to_string(NormalId::Front) + "]", part->frontSurface);
CUBE_MESH->bind();
glDrawArrays(GL_TRIANGLES, 0, CUBE_MESH->vertexCount);
}
@ -283,7 +295,7 @@ void renderAABB() {
ghostShader->set("color", glm::vec3(1.f, 0.f, 0.f));
// Sort by nearest
for (InstanceRef inst : workspace()->GetChildren()) {
for (InstanceRef inst : gWorkspace()->GetChildren()) {
if (inst->GetClass()->className != "Part") continue;
std::shared_ptr<Part> part = std::dynamic_pointer_cast<Part>(inst);
glm::mat4 model = Data::CFrame::IDENTITY + part->cframe.Position();

View file

@ -0,0 +1,23 @@
#include "surface.h"
#include "datatypes/vector.h"
static std::array<Data::Vector3, 6> FACE_NORMALS = {{
{ 1, 0, 0 },
{ 0, 1, 0 },
{ 0, 0, 1 },
{ -1, 0, 0 },
{ 0, -1, 0 },
{ 0, 0, -1 },
}};
NormalId faceFromNormal(Data::Vector3 normal) {
for (int face = 0; face < 6; face++) {
if (normal.Dot(FACE_NORMALS[face]) > 0.99)
return (NormalId)face;
}
return (NormalId)-1;
}
Data::Vector3 normalFromFace(NormalId face) {
return FACE_NORMALS[face];
}

View file

@ -1,5 +1,14 @@
#pragma once
enum NormalId {
Right = 0,
Top = 1,
Back = 2,
Left = 3,
Bottom = 4,
Front = 5
};
enum SurfaceType {
SurfaceSmooth = 0,
SurfaceGlue = 1,
@ -7,4 +16,8 @@ enum SurfaceType {
SurfaceStuds = 3,
SurfaceInlets = 4,
SurfaceUniversal = 5,
};
};
namespace Data { class Vector3; }
NormalId faceFromNormal(Data::Vector3);
Data::Vector3 normalFromFace(NormalId);

View file

@ -11,8 +11,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets LinguistTools)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets LinguistTools)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Multimedia LinguistTools)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Multimedia LinguistTools)
set(TS_FILES editor_en_US.ts)
@ -64,7 +64,7 @@ else()
endif()
target_include_directories(editor PUBLIC "../core/src" "../include")
target_link_libraries(editor PRIVATE openblocks Qt${QT_VERSION_MAJOR}::Widgets)
target_link_libraries(editor PRIVATE openblocks Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Multimedia)
# Qt6 does not include QOpenGLWidgets as part of Widgets base anymore, so
# we have to include it manually

View file

@ -16,6 +16,7 @@
#include <optional>
#include <reactphysics3d/collision/RaycastInfo.h>
#include <vector>
#include <QSound>
#include "datatypes/cframe.h"
#include "datatypes/vector.h"
@ -37,6 +38,7 @@
#include "mainglwidget.h"
#include "math_helper.h"
#include "rendering/surface.h"
static Data::CFrame XYZToZXY(glm::vec3(0, 0, 0), -glm::vec3(1, 0, 0), glm::vec3(0, 0, 1));
@ -130,7 +132,7 @@ bool isMouseDragging = false;
std::optional<std::weak_ptr<Part>> draggingObject;
std::optional<HandleFace> draggingHandle;
void MainGLWidget::handleObjectDrag(QMouseEvent* evt) {
if (!isMouseDragging || !draggingObject) return;
if (!isMouseDragging || !draggingObject || mainWindow()->selectedTool >= TOOL_SMOOTH) return;
QPoint position = evt->pos();
@ -153,11 +155,12 @@ void MainGLWidget::handleObjectDrag(QMouseEvent* evt) {
localFrame = snapCFrame(localFrame);
// Snap to studs
Data::Vector3 draggingPartSize = draggingObject->lock()->size;
Data::Vector3 inverseSurfaceNormal = Data::Vector3::ONE - surfaceNormal.Abs();
glm::vec3 inverseNormalPartSize = (Data::Vector3)(partSize + 1.f) * inverseSurfaceNormal / 2.f;
glm::vec3 inverseNormalPartSize = (Data::Vector3)(partSize + glm::vec3(localFrame.Rotation() * draggingPartSize)) * inverseSurfaceNormal / 2.f;
if (snappingFactor() > 0)
localFrame = localFrame.Rotation() + glm::round(glm::vec3(localFrame.Position() * inverseSurfaceNormal - inverseNormalPartSize) / snappingFactor()) * snappingFactor() + inverseNormalPartSize
+ localFrame.Position() * surfaceNormal.Abs();
+ localFrame.Position() * surfaceNormal.Abs();
Data::CFrame newFrame = targetFrame * localFrame;
@ -214,12 +217,12 @@ void MainGLWidget::handleLinearTransform(QMouseEvent* evt) {
// printf("Post-snap: (%f, %f, %f)\n", diff.x, diff.y, diff.z);
switch (mainWindow()->selectedTool) {
case SelectedTool::MOVE: {
case TOOL_MOVE: {
// Add difference
editorToolHandles->adornee->lock()->cframe = editorToolHandles->adornee->lock()->cframe + diff;
} break;
case SelectedTool::SCALE: {
case TOOL_SCALE: {
// Find local difference
glm::vec3 localDiff = frame.Inverse() * diff;
// Find outwarwd difference
@ -316,11 +319,11 @@ void MainGLWidget::mouseMoveEvent(QMouseEvent* evt) {
handleCursorChange(evt);
switch (mainWindow()->selectedTool) {
case SelectedTool::MOVE:
case SelectedTool::SCALE:
case TOOL_MOVE:
case TOOL_SCALE:
handleLinearTransform(evt);
break;
case SelectedTool::ROTATE:
case TOOL_ROTATE:
handleRotationalTransform(evt);
break;
default:
@ -356,6 +359,28 @@ void MainGLWidget::mousePressEvent(QMouseEvent* evt) {
std::shared_ptr<Part> part = partFromBody(rayHit->body);
if (part->name == "Baseplate") return;
// Handle surface tool
if (mainWindow()->selectedTool >= TOOL_SMOOTH) {
Data::Vector3 localNormal = part->cframe.Inverse().Rotation() * rayHit->worldNormal;
NormalId face = faceFromNormal(localNormal);
SurfaceType surface = SurfaceType(mainWindow()->selectedTool - TOOL_SMOOTH);
switch (face) {
case Right: part->rightSurface = surface; break;
case Top: part->topSurface = surface; break;
case Back: part->backSurface = surface; break;
case Left: part->leftSurface = surface; break;
case Bottom: part->bottomSurface = surface; break;
case Front: part->frontSurface = surface; break;
default: return;
}
if (QFile::exists("./assets/excluded/electronicpingshort.wav"))
QSound::play("./assets/excluded/electronicpingshort.wav");
return;
}
//part.selected = true;
isMouseDragging = true;
draggingObject = part;
@ -400,7 +425,7 @@ void MainGLWidget::keyPressEvent(QKeyEvent* evt) {
else if (evt->key() == Qt::Key_D) moveX = -1;
if (evt->key() == Qt::Key_F) {
workspace()->AddChild(lastPart = Part::New({
gWorkspace()->AddChild(lastPart = Part::New({
.position = camera.cameraPos + camera.cameraFront * glm::vec3(3),
.rotation = glm::vec3(0),
.size = glm::vec3(1, 1, 1),

View file

@ -15,6 +15,7 @@
#include <optional>
#include <qcoreapplication.h>
#include <qglobal.h>
#include <qguiapplication.h>
#include <qicon.h>
#include <qmessagebox.h>
#include <qnamespace.h>
@ -22,6 +23,7 @@
#include <reactphysics3d/engine/PhysicsCommon.h>
#include <reactphysics3d/engine/PhysicsWorld.h>
#include <sstream>
#include <QStyleHints>
#include "common.h"
#include "editorcommon.h"
@ -45,16 +47,37 @@ bool simulationPlaying = false;
bool worldSpaceTransforms = false;
inline bool isDarkMode() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
const auto scheme = QGuiApplication::styleHints()->colorScheme();
return scheme == Qt::ColorScheme::Dark;
#else
const QPalette defaultPalette;
const auto text = defaultPalette.color(QPalette::WindowText);
const auto window = defaultPalette.color(QPalette::Window);
return text.lightness() > window.lightness();
#endif // QT_VERSION
}
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
dataModel->Init();
gDataModel->Init();
ui->setupUi(this);
timer.start(33, this);
setMouseTracking(true);
// https://stackoverflow.com/a/78854851/16255372
QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() + QStringList { "./assets/icons" });
if (isDarkMode())
QIcon::setFallbackThemeName("editor-dark");
else
QIcon::setFallbackThemeName("editor");
setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);
setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
@ -86,12 +109,19 @@ MainWindow::MainWindow(QWidget *parent)
ui->explorerView->buildContextMenu();
connect(ui->actionToolSelect, &QAction::triggered, this, [&]() { selectedTool = SelectedTool::SELECT; updateToolbars(); });
connect(ui->actionToolMove, &QAction::triggered, this, [&](bool state) { selectedTool = state ? SelectedTool::MOVE : SelectedTool::SELECT; updateToolbars(); });
connect(ui->actionToolScale, &QAction::triggered, this, [&](bool state) { selectedTool = state ? SelectedTool::SCALE : SelectedTool::SELECT; updateToolbars(); });
connect(ui->actionToolRotate, &QAction::triggered, this, [&](bool state) { selectedTool = state ? SelectedTool::ROTATE : SelectedTool::SELECT; updateToolbars(); });
connect(ui->actionToolSelect, &QAction::triggered, this, [&]() { selectedTool = TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolMove, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_MOVE : TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolScale, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_SCALE : TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolRotate, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_ROTATE : TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolSmooth, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_SMOOTH : TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolGlue, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_GLUE : TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolWeld, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_WELD : TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolStuds, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_STUDS : TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolInlets, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_INLETS : TOOL_SELECT; updateToolbars(); });
connect(ui->actionToolUniversal, &QAction::triggered, this, [&](bool state) { selectedTool = state ? TOOL_UNIVERSAL : TOOL_SELECT; updateToolbars(); });
ui->actionToolSelect->setChecked(true);
selectedTool = SelectedTool::SELECT;
selectedTool = TOOL_SELECT;
connect(ui->actionGridSnap1, &QAction::triggered, this, [&]() { snappingMode = GridSnappingMode::SNAP_1_STUD; updateToolbars(); });
connect(ui->actionGridSnap05, &QAction::triggered, this, [&]() { snappingMode = GridSnappingMode::SNAP_05_STUDS; updateToolbars(); });
@ -116,9 +146,11 @@ MainWindow::MainWindow(QWidget *parent)
worldSpaceTransforms = !worldSpaceTransforms;
updateToolbars();
if (worldSpaceTransforms) {
ui->actionToggleSpace->setText("W");
ui->actionToggleSpace->setText("World");
ui->actionToggleSpace->setIcon(QIcon::fromTheme("space-global"));
} else {
ui->actionToggleSpace->setText("L");
ui->actionToggleSpace->setText("Local");
ui->actionToggleSpace->setIcon(QIcon::fromTheme("space-local"));
}
});
@ -136,11 +168,11 @@ MainWindow::MainWindow(QWidget *parent)
if (result == QMessageBox::Cancel) return;
if (result == QMessageBox::Save) {
std::optional<std::string> path;
if (!dataModel->HasFile())
path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptSave, QString::fromStdString("Save " + dataModel->name));
if (!gDataModel->HasFile())
path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptSave, QString::fromStdString("Save " + gDataModel->name));
if (!path || path == "") return;
dataModel->SaveToFile(path);
gDataModel->SaveToFile(path);
}
#endif
@ -150,15 +182,15 @@ MainWindow::MainWindow(QWidget *parent)
// TL;DR: This stinks and I need to fix it.)
ui->mainWidget->lastPart = Part::New();
dataModel = DataModel::New();
dataModel->Init();
ui->explorerView->updateRoot(dataModel);
gDataModel = DataModel::New();
gDataModel->Init();
ui->explorerView->updateRoot(gDataModel);
// TODO: Remove this and use a proper fix. This *WILL* cause a leak and memory issues in the future
simulationInit();
// Baseplate
workspace()->AddChild(ui->mainWidget->lastPart = Part::New({
gWorkspace()->AddChild(ui->mainWidget->lastPart = Part::New({
.position = glm::vec3(0, -5, 0),
.rotation = glm::vec3(0),
.size = glm::vec3(512, 1.2, 512),
@ -171,25 +203,30 @@ MainWindow::MainWindow(QWidget *parent)
connect(ui->actionSave, &QAction::triggered, this, [&]() {
std::optional<std::string> path;
if (!dataModel->HasFile())
path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptSave, QString::fromStdString("Save " + dataModel->name));
if (!gDataModel->HasFile())
path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptSave, QString::fromStdString("Save " + gDataModel->name));
if (!path || path == "") return;
dataModel->SaveToFile(path);
gDataModel->SaveToFile(path);
});
connect(ui->actionSaveAs, &QAction::triggered, this, [&]() {
std::optional<std::string> path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptSave, QString::fromStdString("Save as " + dataModel->name));
std::optional<std::string> path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptSave, QString::fromStdString("Save as " + gDataModel->name));
if (!path || path == "") return;
dataModel->SaveToFile(path);
gDataModel->SaveToFile(path);
});
connect(ui->actionOpen, &QAction::triggered, this, [&]() {
std::optional<std::string> path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptOpen);
if (!path || path == "") return;
// // See TODO: Also remove this (the reaso
// ui->mainWidget->lastPart = Part::New();
// simulationInit();
std::shared_ptr<DataModel> newModel = DataModel::LoadFromFile(path.value());
dataModel = newModel;
gDataModel = newModel;
ui->explorerView->updateRoot(newModel);
});
@ -242,7 +279,7 @@ MainWindow::MainWindow(QWidget *parent)
for (pugi::xml_node instNode : rootDoc.children()) {
InstanceRef inst = Instance::Deserialize(&instNode);
workspace()->AddChild(inst);
gWorkspace()->AddChild(inst);
}
});
@ -322,7 +359,7 @@ MainWindow::MainWindow(QWidget *parent)
simulationInit();
// Baseplate
workspace()->AddChild(ui->mainWidget->lastPart = Part::New({
gWorkspace()->AddChild(ui->mainWidget->lastPart = Part::New({
.position = glm::vec3(0, -5, 0),
.rotation = glm::vec3(0),
.size = glm::vec3(512, 1.2, 512),
@ -332,7 +369,7 @@ MainWindow::MainWindow(QWidget *parent)
ui->mainWidget->lastPart->name = "Baseplate";
syncPartPhysics(ui->mainWidget->lastPart);
workspace()->AddChild(ui->mainWidget->lastPart = Part::New({
gWorkspace()->AddChild(ui->mainWidget->lastPart = Part::New({
.position = glm::vec3(0),
.rotation = glm::vec3(0.5, 2, 1),
.size = glm::vec3(4, 1.2, 2),
@ -354,11 +391,11 @@ void MainWindow::closeEvent(QCloseEvent* evt) {
if (result == QMessageBox::Cancel) return evt->ignore();
if (result == QMessageBox::Save) {
std::optional<std::string> path;
if (!dataModel->HasFile())
path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptSave, QString::fromStdString("Save " + dataModel->name));
if (!gDataModel->HasFile())
path = openFileDialog("Openblocks Level (*.obl)", ".obl", QFileDialog::AcceptSave, QString::fromStdString("Save " + gDataModel->name));
if (!path || path == "") return evt->ignore();
dataModel->SaveToFile(path);
gDataModel->SaveToFile(path);
}
#endif
}
@ -391,23 +428,30 @@ void MainWindow::timerEvent(QTimerEvent* evt) {
}
void MainWindow::updateToolbars() {
ui->actionToolSelect->setChecked(selectedTool == SelectedTool::SELECT);
ui->actionToolMove->setChecked(selectedTool == SelectedTool::MOVE);
ui->actionToolScale->setChecked(selectedTool == SelectedTool::SCALE);
ui->actionToolRotate->setChecked(selectedTool == SelectedTool::ROTATE);
ui->actionToolSelect->setChecked(selectedTool == TOOL_SELECT);
ui->actionToolMove->setChecked(selectedTool == TOOL_MOVE);
ui->actionToolScale->setChecked(selectedTool == TOOL_SCALE);
ui->actionToolRotate->setChecked(selectedTool == TOOL_ROTATE);
ui->actionToolSmooth->setChecked(selectedTool == TOOL_SMOOTH);
ui->actionToolGlue->setChecked(selectedTool == TOOL_GLUE);
ui->actionToolWeld->setChecked(selectedTool == TOOL_WELD);
ui->actionToolStuds->setChecked(selectedTool == TOOL_STUDS);
ui->actionToolInlets->setChecked(selectedTool == TOOL_INLETS);
ui->actionToolUniversal->setChecked(selectedTool == TOOL_UNIVERSAL);
ui->actionGridSnap1->setChecked(snappingMode == GridSnappingMode::SNAP_1_STUD);
ui->actionGridSnap05->setChecked(snappingMode == GridSnappingMode::SNAP_05_STUDS);
ui->actionGridSnapOff->setChecked(snappingMode == GridSnappingMode::SNAP_OFF);
editorToolHandles->worldMode = (selectedTool == SelectedTool::SCALE || selectedTool == SelectedTool::ROTATE) ? false : worldSpaceTransforms;
editorToolHandles->nixAxes = selectedTool == SelectedTool::ROTATE;
editorToolHandles->worldMode = (selectedTool == TOOL_SCALE || selectedTool == TOOL_ROTATE) ? false : worldSpaceTransforms;
editorToolHandles->nixAxes = selectedTool == TOOL_ROTATE;
editorToolHandles->active = selectedTool != SelectedTool::SELECT;
editorToolHandles->active = selectedTool > TOOL_SELECT && selectedTool < TOOL_SMOOTH;
editorToolHandles->handlesType =
selectedTool == SelectedTool::MOVE ? HandlesType::MoveHandles
: selectedTool == SelectedTool::SCALE ? HandlesType::ScaleHandles
: selectedTool == SelectedTool::ROTATE ? HandlesType::RotateHandles
selectedTool == TOOL_MOVE ? HandlesType::MoveHandles
: selectedTool == TOOL_SCALE ? HandlesType::ScaleHandles
: selectedTool == TOOL_ROTATE ? HandlesType::RotateHandles
: HandlesType::ScaleHandles;
}

View file

@ -11,10 +11,17 @@
#include <qfiledialog.h>
enum SelectedTool {
SELECT,
MOVE,
SCALE,
ROTATE,
TOOL_SELECT,
TOOL_MOVE,
TOOL_SCALE,
TOOL_ROTATE,
TOOL_SMOOTH,
TOOL_GLUE,
TOOL_WELD,
TOOL_STUDS,
TOOL_INLETS,
TOOL_UNIVERSAL,
};
enum GridSnappingMode {

View file

@ -99,46 +99,6 @@
</layout>
</widget>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionNew"/>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="separator"/>
<addaction name="actionAddPart"/>
<addaction name="separator"/>
<addaction name="actionToolSelect"/>
<addaction name="actionToolMove"/>
<addaction name="actionToolScale"/>
<addaction name="actionToolRotate"/>
<addaction name="actionToggleSpace"/>
<addaction name="separator"/>
<addaction name="actionDelete"/>
<addaction name="actionCopy"/>
<addaction name="actionCut"/>
<addaction name="actionPaste"/>
<addaction name="actionPasteInto"/>
<addaction name="separator"/>
<addaction name="actionGridSnap1"/>
<addaction name="actionGridSnap05"/>
<addaction name="actionGridSnapOff"/>
<addaction name="separator"/>
<addaction name="actionToggleSimulation"/>
</widget>
<widget class="QDockWidget" name="outputWidget">
<property name="windowTitle">
<string>Output</string>
@ -158,6 +118,95 @@
</layout>
</widget>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionNew"/>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
</widget>
<widget class="QToolBar" name="toolBar_2">
<property name="windowTitle">
<string>toolBar_2</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionToolSelect"/>
<addaction name="actionToolMove"/>
<addaction name="actionToolScale"/>
<addaction name="actionToolRotate"/>
<addaction name="actionToggleSpace"/>
</widget>
<widget class="QToolBar" name="toolBar_3">
<property name="windowTitle">
<string>toolBar_3</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionDelete"/>
<addaction name="actionCopy"/>
<addaction name="actionCut"/>
<addaction name="actionPaste"/>
<addaction name="actionPasteInto"/>
</widget>
<widget class="QToolBar" name="toolBar_4">
<property name="windowTitle">
<string>toolBar_4</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionGridSnap1"/>
<addaction name="actionGridSnap05"/>
<addaction name="actionGridSnapOff"/>
</widget>
<widget class="QToolBar" name="toolBar_5">
<property name="windowTitle">
<string>toolBar_5</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionToggleSimulation"/>
</widget>
<widget class="QToolBar" name="toolBar_6">
<property name="windowTitle">
<string>toolBar_6</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionToolSmooth"/>
<addaction name="actionToolGlue"/>
<addaction name="actionToolWeld"/>
<addaction name="actionToolStuds"/>
<addaction name="actionToolInlets"/>
<addaction name="actionToolUniversal"/>
</widget>
<action name="actionAddPart">
<property name="icon">
<iconset>
@ -300,8 +349,7 @@
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>assets/icons/editor/snap1.png</normaloff>assets/icons/editor/snap1.png</iconset>
<iconset theme="snap1"/>
</property>
<property name="text">
<string>1-Stud Snapping</string>
@ -318,8 +366,7 @@
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>assets/icons/editor/snap05.png</normaloff>assets/icons/editor/snap05.png</iconset>
<iconset theme="snap05"/>
</property>
<property name="text">
<string>1/2-Stud Snapping</string>
@ -336,8 +383,7 @@
<bool>true</bool>
</property>
<property name="icon">
<iconset>
<normaloff>assets/icons/editor/snapoff.png</normaloff>assets/icons/editor/snapoff.png</iconset>
<iconset theme="snapoff"/>
</property>
<property name="text">
<string>No Grid Snapping</string>
@ -468,8 +514,11 @@
</property>
</action>
<action name="actionToggleSpace">
<property name="icon">
<iconset theme="space-local"/>
</property>
<property name="text">
<string>L</string>
<string>Local</string>
</property>
<property name="toolTip">
<string>Switch between local and world space transformations</string>
@ -495,6 +544,108 @@
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionToolStuds">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="surface-studs"/>
</property>
<property name="text">
<string>Studs</string>
</property>
<property name="toolTip">
<string>Studs</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionToolInlets">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="surface-inlets"/>
</property>
<property name="text">
<string>Inlets</string>
</property>
<property name="toolTip">
<string>Inlets</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionToolUniversal">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="surface-universal"/>
</property>
<property name="text">
<string>Universal</string>
</property>
<property name="toolTip">
<string>Universal</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionToolSmooth">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="surface-smooth"/>
</property>
<property name="text">
<string>Smooth</string>
</property>
<property name="toolTip">
<string>Smooth</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionToolWeld">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="surface-weld"/>
</property>
<property name="text">
<string>Weld</string>
</property>
<property name="toolTip">
<string>Weld</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionToolGlue">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="surface-glue"/>
</property>
<property name="text">
<string>Glue</string>
</property>
<property name="toolTip">
<string>Glue</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View file

@ -13,7 +13,7 @@
ExplorerView::ExplorerView(QWidget* parent):
QTreeView(parent),
model(ExplorerModel(std::dynamic_pointer_cast<Instance>(dataModel))) {
model(ExplorerModel(std::dynamic_pointer_cast<Instance>(gDataModel))) {
this->setModel(&model);
// Disabling the root decoration will cause the expand/collapse chevrons to be hidden too, we don't want that
@ -29,7 +29,7 @@ ExplorerView::ExplorerView(QWidget* parent):
this->setContextMenuPolicy(Qt::CustomContextMenu);
// Expand workspace
this->expand(model.ObjectToIndex(workspace()));
this->expand(model.ObjectToIndex(gWorkspace()));
connect(this, &QTreeView::customContextMenuRequested, this, [&](const QPoint& point) {
QModelIndex index = this->indexAt(point);