diff --git a/resources/data/embossed_text.obj b/resources/data/embossed_text.obj new file mode 100644 index 0000000000..8f6f09f41e --- /dev/null +++ b/resources/data/embossed_text.obj @@ -0,0 +1,2555 @@ +v -18.02 2 -1 +v -17.5 -0.13 -1 +v -18.02 -1.94 -1 +v 5.62 0.93 -1 +v 5.93 0.58 -1 +v 5.65 0.49 -1 +v -17.5 0.33 -1 +v -15.07 -1.94 -1 +v -17.5 -1.47 -1 +v -15.31 -0.13 -1 +v -15.31 0.33 -1 +v -17.5 1.54 -1 +v -11.91 0.85 -1 +v -11.73 0.54 -1 +v -12.12 0.7 -1 +v -15.16 1.54 -1 +v -15.16 2 -1 +v -15.07 -1.47 -1 +v 6.78 -1.58 -1 +v 6.62 -1.76 -1 +v 6.37 -1.52 -1 +v 6.69 -1.14 -1 +v 6.56 -1.35 -1 +v -9.33 0.59 -1 +v -9.81 2 -1 +v -9.33 2 -1 +v 6.78 -1.94 -1 +v 7.23 -1.94 -1 +v 6.74 2 -1 +v 7.23 2 -1 +v 6.74 0.59 -1 +v 5.95 0.98 -1 +v 2.05 -1.62 -1 +v 2.5 -1.41 -1 +v 2.25 -1.79 -1 +v 5.33 0.1 -1 +v 5.24 -0.34 -1 +v 4.78 -0.1 -1 +v -6.35 -1.79 -1 +v -6.25 -1 -1 +v -6 -1.5 -1 +v 4.74 -0.51 -1 +v 5.27 -0.88 -1 +v -4.84 -1.11 -1 +v -4.74 -0.66 -1 +v -4.26 -0.84 -1 +v 6.2 0.95 -1 +v 6.5 0.83 -1 +v 6.16 0.56 -1 +v 5.09 -1.59 -1 +v 4.9 -1.29 -1 +v 5.41 -1 -1 +v 5.34 -1.82 -1 +v 5.65 -1.5 -1 +v 5.87 -1.59 -1 +v 5.65 -1.96 -1 +v 3.96 -1.69 -1 +v 3.68 -1.28 -1 +v 4.2 -1.37 -1 +v 5.98 -2 -1 +v 3.81 -1.02 -1 +v 6.44 -1.9 -1 +v 4.78 -0.92 -1 +v 4.89 0.26 -1 +v 5 0.49 -1 +v 5.46 0.31 -1 +v 5.25 0.75 -1 +v 6.38 0.47 -1 +v 6.62 0.22 -1 +v 6.76 -0.15 -1 +v 10.39 -1.52 -1 +v 10.65 -1.93 -1 +v 10.36 -1.98 -1 +v 6.78 -0.72 -1 +v 6.09 -1.61 -1 +v 6.22 -1.98 -1 +v -8.06 -1.33 -1 +v -7.92 -1.11 -1 +v -7.69 -1.6 -1 +v 3.54 0.88 -1 +v 3.45 0.47 -1 +v 3.15 0.98 -1 +v 2.49 -1.91 -1 +v 2.78 -1.57 -1 +v -9.02 -1.9 -1 +v -9.2 -1.77 -1 +v -8.9 -1.55 -1 +v -7.85 -0.13 -1 +v -7.33 -0.27 -1 +v -7.83 -0.68 -1 +v 2.35 -1.21 -1 +v -7.83 0.74 -1 +v -8.22 0.47 -1 +v -8.04 0.87 -1 +v -8.94 0.49 -1 +v -9.19 0.24 -1 +v -8.29 0.95 -1 +v -8.49 0.58 -1 +v -8.69 0.98 -1 +v 2.23 -0.95 -1 +v 1.69 -0.75 -1 +v 2.19 -0.63 -1 +v 2.21 -0.23 -1 +v -4.33 -1.16 -1 +v -4.43 -1.4 -1 +v -7.98 0.23 -1 +v 4.12 0.37 -1 +v 3.77 0 -1 +v 3.67 0.25 -1 +v -12.82 0.95 -1 +v -13.02 0.53 -1 +v -13.11 0.98 -1 +v -9.33 -0.09 -1 +v -8.54 -1.61 -1 +v -8.81 -1.98 -1 +v -13.91 -0.15 -1 +v -13.98 0.51 -1 +v -13.85 0.14 -1 +v -9.32 -1.02 -1 +v -9.18 -1.31 -1 +v -9.36 -1.58 -1 +v -9.81 -1.94 -1 +v -9.36 -1.94 -1 +v -8.42 -2 -1 +v -8.26 -1.51 -1 +v -7.42 -1.12 -1 +v -12.3 0.48 -1 +v -11.97 0.41 -1 +v -9.09 0.83 -1 +v -8.74 0.56 -1 +v -7.66 0.57 -1 +v -7.47 0.27 -1 +v -7.35 -0.81 -1 +v -7.53 -1.38 -1 +v -7.97 -1.84 -1 +v 10.58 -1.51 -1 +v 17.42 -1.96 -1 +v 17.58 -1.5 -1 +v 17.73 -1.98 -1 +v 17.47 -1.37 -1 +v 17.02 -1.64 -1 +v 16.98 -1.27 -1 +v 10.22 -1.5 -1 +v 9.62 -1.27 -1 +v 10.11 -1.37 -1 +v 9.65 -1.64 -1 +v 9.26 0.91 -1 +v 9.61 0.54 -1 +v 9.26 0.54 -1 +v -9.37 -0.58 -1 +v 9.61 1.62 -1 +v 10.1 0.91 -1 +v 9.61 0.91 -1 +v 10.58 0.91 -1 +v 10.58 0.54 -1 +v 10.1 0.54 -1 +v 10.1 1.92 -1 +v 9.82 -1.86 -1 +v 10.05 -1.96 -1 +v 12.64 -1.97 -1 +v 12.11 -2 -1 +v 12.26 -1.61 -1 +v 17.76 -1.52 -1 +v 17.95 -1.51 -1 +v 18.02 -1.93 -1 +v 16.98 0.54 -1 +v 16.63 0.91 -1 +v 16.98 0.91 -1 +v 17.46 0.54 -1 +v 17.46 0.91 -1 +v 12.51 -1.57 -1 +v 16.98 1.62 -1 +v 17.95 0.91 -1 +v 17.95 0.54 -1 +v 17.46 1.92 -1 +v 16.63 0.54 -1 +v 12.89 -1.28 -1 +v 13.41 -1.37 -1 +v 13.17 -1.69 -1 +v 13.02 -1.02 -1 +v 17.19 -1.86 -1 +v 15.13 -0.06 -1 +v 15.83 0.91 -1 +v 16.41 0.91 -1 +v 15.42 -0.42 -1 +v 12.72 -1.46 -1 +v 15.15 -0.81 -1 +v 14.39 -1.94 -1 +v 14.85 -0.45 -1 +v 13.52 -1.08 -1 +v 12.91 -1.87 -1 +v 11.46 -1.79 -1 +v 11.71 -1.41 -1 +v 11.7 -1.91 -1 +v 11.43 -0.23 -1 +v 11.4 -0.63 -1 +v 10.9 -0.75 -1 +v 11.99 -1.57 -1 +v 11.26 -1.62 -1 +v 11.56 -1.21 -1 +v 11.1 -1.4 -1 +v 11.44 -0.95 -1 +v 10.99 -1.15 -1 +v 14.49 0.91 -1 +v 13.89 0.91 -1 +v -14.41 0.91 -1 +v 11.48 0.04 -1 +v 10.92 -0.19 -1 +v 10.99 0.1 -1 +v 13.02 -0.23 -1 +v 11.11 0.36 -1 +v 11.58 0.24 -1 +v 11.27 0.58 -1 +v 11.74 0.42 -1 +v 11.47 0.75 -1 +v 12.01 0.55 -1 +v 11.7 0.88 -1 +v 11.95 0.95 -1 +v 12.36 0.58 -1 +v 12.36 0.98 -1 +v 12.66 0.47 -1 +v 12.76 0.88 -1 +v 12.98 0.76 -1 +v 12.88 0.25 -1 +v 13.17 0.59 -1 +v 13.33 0.37 -1 +v 12.99 0 -1 +v 13.48 0.02 -1 +v 13.54 -0.63 -1 +v -13.75 0.32 -1 +v -13.6 0.45 -1 +v -13.32 0.56 -1 +v -13.62 0.85 -1 +v -13.38 0.95 -1 +v -12.85 0.43 -1 +v -12.75 0.25 -1 +v -12.41 0.69 -1 +v -12.14 0.2 -1 +v -12.22 -0.1 -1 +v -12.71 0.02 -1 +v -10.83 0.81 -1 +v -11.23 0.48 -1 +v -11.13 0.95 -1 +v -11.67 0.95 -1 +v -12.71 -1.94 -1 +v -12.22 -1.94 -1 +v -11.02 0.06 -1 +v -11.08 0.33 -1 +v -10.54 0.21 -1 +v -11.41 0.98 -1 +v -11.41 0.55 -1 +v -10.53 -1.94 -1 +v -11.01 -1.94 -1 +v -10.63 0.56 -1 +v -14.41 -1.94 -1 +v -12.59 0.85 -1 +v -13.82 0.7 -1 +v -13.98 0.91 -1 +v 3.81 -0.23 -1 +v 4.33 -0.63 -1 +v -13.93 -1.94 -1 +v 3.51 -1.46 -1 +v 4.31 -1.08 -1 +v 3.43 -1.97 -1 +v 3.7 -1.87 -1 +v 3.3 -1.57 -1 +v 3.05 -1.61 -1 +v 1.89 -1.4 -1 +v 1.71 -0.19 -1 +v 2.26 0.04 -1 +v 3.96 0.59 -1 +v 3.77 0.76 -1 +v 2.74 0.95 -1 +v 2.8 0.55 -1 +v -4.97 -1.33 -1 +v 2.48 0.88 -1 +v 2.53 0.42 -1 +v 2.06 0.58 -1 +v 2.36 0.24 -1 +v 1.89 0.36 -1 +v 1.78 0.1 -1 +v -5.73 0.98 -1 +v -5.47 0.57 -1 +v -5.75 0.56 -1 +v -2.89 -1.58 -1 +v -2.57 -1.61 -1 +v -2.41 -2 -1 +v 0.62 0.31 -1 +v 0.84 0.77 -1 +v 0.99 0.62 -1 +v 1.78 -1.15 -1 +v 1.1 0.43 -1 +v -5.86 -1.98 -1 +v -6.12 -1.91 -1 +v 0.62 -0.88 -1 +v 0.4 -0.8 -1 +v 0.73 -0.38 -1 +v 2.9 -2 -1 +v 16.49 -1.94 -1 +v 15.89 -1.94 -1 +v 2.26 0.75 -1 +v 3.14 0.58 -1 +v 4.27 0.02 -1 +v -0.56 -1.87 -1 +v -0.37 -1.47 -1 +v -0.25 -1.98 -1 +v -4.24 -0.23 -1 +v -0.47 0.01 -1 +v -0.78 -0.38 -1 +v -0.91 -0.22 -1 +v -5.04 0.88 -1 +v -5.16 0.47 -1 +v -5.29 0.96 -1 +v 0.95 -1.75 -1 +v 0.72 -1.89 -1 +v 0.61 -1.47 -1 +v -0.5 -0.54 -1 +v -0.28 -0.09 -1 +v -5.97 0.48 -1 +v 1.13 -1.56 -1 +v 0.73 -1.32 -1 +v -6.67 0.44 -1 +v -6.18 0.31 -1 +v -6.32 0.09 -1 +v 0.77 -1.15 -1 +v 1.23 -1.33 -1 +v 0.5 0.46 -1 +v 0.26 0.57 -1 +v 0.62 0.88 -1 +v -0.55 0.18 -1 +v -1.01 0.03 -1 +v -4.45 0.38 -1 +v -4.33 0.13 -1 +v -4.9 0.21 -1 +v -4.76 -0.13 -1 +v -6.71 -1.4 -1 +v -6.55 -1.62 -1 +v -5.69 -1.6 -1 +v -5.58 -2 -1 +v -4.65 -1.67 -1 +v -5.19 -1.51 -1 +v -5.22 -1.96 -1 +v -5.4 -1.59 -1 +v -6.39 -0.88 -1 +v -6.83 -1.15 -1 +v -6.92 -0.72 -1 +v -6.42 -0.34 -1 +v -6.89 -0.14 -1 +v -6.81 0.18 -1 +v -6.48 0.66 -1 +v -6.2 0.85 -1 +v -4.81 0.76 -1 +v -4.61 0.59 -1 +v -4.89 -1.83 -1 +v 1.16 0.18 -1 +v 0.69 0.11 -1 +v -0.98 0.41 -1 +v -1.52 -1.33 -1 +v -1.63 -1.56 -1 +v -2.02 -1.32 -1 +v -1.51 -0.87 -1 +v -1.98 -1.15 -1 +v -2.02 -1 -1 +v -0.5 0.88 -1 +v -0.37 0.5 -1 +v -0.78 0.71 -1 +v -0.15 0.57 -1 +v -0.09 0.98 -1 +v 0.34 0.96 -1 +v -2.92 0.57 -1 +v -3.27 0.88 -1 +v -2.86 0.98 -1 +v -2.49 0.57 -1 +v -3.54 0.71 -1 +v -3.13 0.5 -1 +v -3.28 0.35 -1 +v -0.51 0.35 -1 +v -3.26 -0.54 -1 +v -3.05 -0.09 -1 +v -2.35 -0.8 -1 +v -1.8 -1.75 -1 +v -2.03 -1.89 -1 +v -2.14 -1.47 -1 +v -0.78 -1.73 -1 +v -0.54 -1.26 -1 +v -1 -1.42 -1 +v 0.18 -1.61 -1 +v 0.46 -1.56 -1 +v 0.34 -2 -1 +v 1.24 -0.87 -1 +v 1.27 -1.1 -1 +v 1.02 -0.54 -1 +v 0.73 -1 -1 +v 1.15 -0.68 -1 +v -3.13 -1.47 -1 +v -3.01 -1.98 -1 +v -3.33 -1.87 -1 +v -0.62 -1.01 -1 +v -2.29 -1.56 -1 +v -1.1 -1.08 -1 +v -2.02 -0.38 -1 +v -2.13 -0.88 -1 +v -0.13 -1.58 -1 +v -1.74 -0.54 -1 +v -2.13 0.31 -1 +v -1.65 0.43 -1 +v -2.06 0.11 -1 +v -3.23 0.01 -1 +v -3.54 -0.38 -1 +v -3.38 -1.01 -1 +v -3.3 -1.26 -1 +v -3.76 -1.42 -1 +v -3.67 -0.22 -1 +v -3.78 0.03 -1 +v -3.74 0.41 -1 +v -3.31 0.18 -1 +v -2.41 0.96 -1 +v -2.13 0.88 -1 +v -2.25 0.46 -1 +v -1.91 0.77 -1 +v -1.76 0.62 -1 +v -1.59 0.18 -1 +v -1.48 -1.1 -1 +v -1.6 -0.68 -1 +v -3.55 -1.73 -1 +v -3.86 -1.08 -1 +v 13.81 -1.94 -1 +v -18.02 2 1 +v -18.02 -1.94 1 +v -17.5 -0.13 1 +v 5.62 0.93 1 +v 5.63 0.47 1 +v 5.9 0.58 1 +v -17.5 0.33 1 +v -15.07 -1.94 1 +v -17.5 -1.47 1 +v -15.31 -0.13 1 +v -15.31 0.33 1 +v -17.5 1.54 1 +v -11.91 0.85 1 +v -12.12 0.7 1 +v -11.75 0.53 1 +v -15.16 1.54 1 +v -15.16 2 1 +v -15.07 -1.47 1 +v 6.78 -1.58 1 +v 6.39 -1.5 1 +v 6.62 -1.76 1 +v -9.33 0.59 1 +v -9.33 2 1 +v -9.81 2 1 +v 6.78 -1.94 1 +v 7.23 -1.94 1 +v 6.74 2 1 +v 6.74 0.59 1 +v 7.23 2 1 +v 5.95 0.98 1 +v 2.05 -1.62 1 +v 2.25 -1.79 1 +v 2.52 -1.43 1 +v -7.33 -0.33 1 +v -7.92 0.1 1 +v -7.83 -0.33 1 +v 5.4 0.23 1 +v 4.89 0.26 1 +v 5.27 -0.13 1 +v 4.74 -0.51 1 +v 5.24 -0.68 1 +v -6.35 -1.79 1 +v -5.97 -1.51 1 +v -6.18 -1.33 1 +v 4.78 -0.92 1 +v 5.34 -1.12 1 +v 6.2 0.95 1 +v 6.16 0.56 1 +v 6.53 0.81 1 +v 5.09 -1.59 1 +v 4.9 -1.29 1 +v 5.34 -1.82 1 +v 5.67 -1.51 1 +v 5.47 -1.33 1 +v 5.87 -1.59 1 +v 5.65 -1.96 1 +v 3.93 -1.71 1 +v 4.18 -1.4 1 +v 3.68 -1.28 1 +v 5.98 -2 1 +v 3.81 -1.02 1 +v 6.44 -1.9 1 +v 4.78 -0.1 1 +v 5.02 0.51 1 +v 5.27 0.77 1 +v 6.36 0.48 1 +v 6.61 0.24 1 +v 6.75 -0.11 1 +v 10.4 -1.52 1 +v 10.34 -1.98 1 +v 10.65 -1.93 1 +v 6.73 -1.02 1 +v 6.78 -0.68 1 +v 6.63 -1.26 1 +v 6.11 -1.6 1 +v 6.22 -1.98 1 +v 3.54 0.88 1 +v 3.2 0.98 1 +v 3.42 0.48 1 +v -7.99 -1.24 1 +v -7.53 -1.38 1 +v -7.85 -0.88 1 +v 2.49 -1.91 1 +v 2.81 -1.58 1 +v -9.02 -1.9 1 +v -9.02 -1.48 1 +v -9.2 -1.77 1 +v 2.35 -1.21 1 +v -7.83 0.74 1 +v -8.04 0.87 1 +v -8.24 0.49 1 +v -9.21 0.21 1 +v -8.96 0.47 1 +v -8.29 0.95 1 +v -8.65 0.98 1 +v -8.52 0.58 1 +v 2.24 -0.98 1 +v 2.19 -0.63 1 +v 1.71 -0.86 1 +v 2.21 -0.23 1 +v 1.69 -0.31 1 +v -4.33 -1.16 1 +v -4.76 -0.87 1 +v -4.45 -1.43 1 +v -8.05 0.31 1 +v 4.12 0.37 1 +v 3.66 0.26 1 +v 3.77 0.03 1 +v -12.82 0.95 1 +v -13.11 0.98 1 +v -13.04 0.54 1 +v -9.37 -0.35 1 +v -8.52 -1.6 1 +v -8.79 -1.58 1 +v -8.81 -1.98 1 +v -9.31 -0.02 1 +v -13.92 -0.18 1 +v -13.85 0.14 1 +v -13.98 0.51 1 +v -9.81 -1.94 1 +v -9.36 -1.94 1 +v -9.36 -1.58 1 +v -8.57 -2 1 +v -8.23 -1.5 1 +v -8.32 -1.98 1 +v -7.39 -1.02 1 +v -9.25 -1.19 1 +v -9.35 -0.87 1 +v -12.3 0.48 1 +v -11.99 0.39 1 +v -9.06 0.85 1 +v -8.74 0.56 1 +v -7.66 0.57 1 +v -7.45 0.23 1 +v -7.69 -1.6 1 +v -8.01 -1.87 1 +v 10.58 -1.51 1 +v 17.42 -1.96 1 +v 17.7 -1.98 1 +v 17.59 -1.5 1 +v 17.48 -1.38 1 +v 16.98 -1 1 +v 17.02 -1.62 1 +v 10.23 -1.5 1 +v 9.61 -1 1 +v 9.65 -1.62 1 +v 10.11 -1.38 1 +v 9.26 0.91 1 +v 9.26 0.54 1 +v 9.61 0.54 1 +v 9.61 1.62 1 +v 9.61 0.91 1 +v 10.1 0.91 1 +v 10.58 0.54 1 +v 10.58 0.91 1 +v 10.1 0.54 1 +v 10.1 1.92 1 +v 9.8 -1.84 1 +v 10.05 -1.96 1 +v 12.6 -1.98 1 +v 12.26 -1.61 1 +v 12.06 -2 1 +v 17.77 -1.52 1 +v 18.02 -1.93 1 +v 17.95 -1.51 1 +v 16.98 0.54 1 +v 16.98 0.91 1 +v 16.63 0.91 1 +v 17.46 0.54 1 +v 17.46 0.91 1 +v 12.51 -1.57 1 +v 16.98 1.62 1 +v 17.95 0.54 1 +v 17.95 0.91 1 +v 17.46 1.92 1 +v 16.63 0.54 1 +v 12.89 -1.28 1 +v 13.15 -1.71 1 +v 13.4 -1.4 1 +v 13.02 -1.02 1 +v 17.16 -1.84 1 +v 15.13 -0.06 1 +v 16.41 0.91 1 +v 15.83 0.91 1 +v 15.42 -0.42 1 +v 12.72 -1.46 1 +v 15.15 -0.81 1 +v 14.85 -0.45 1 +v 14.39 -1.94 1 +v 13.52 -1.08 1 +v 12.91 -1.87 1 +v 11.46 -1.79 1 +v 11.7 -1.91 1 +v 11.74 -1.43 1 +v 11.43 -0.23 1 +v 10.9 -0.31 1 +v 11.4 -0.63 1 +v 12.02 -1.58 1 +v 11.26 -1.62 1 +v 11.56 -1.21 1 +v 11.1 -1.4 1 +v 11.45 -0.98 1 +v 10.99 -1.15 1 +v 10.92 -0.86 1 +v 14.49 0.91 1 +v 13.89 0.91 1 +v -14.41 0.91 1 +v 11.47 0.01 1 +v 10.99 0.1 1 +v 13.02 -0.23 1 +v 11.11 0.36 1 +v 11.58 0.24 1 +v 11.27 0.58 1 +v 11.72 0.4 1 +v 11.47 0.75 1 +v 11.99 0.55 1 +v 11.7 0.88 1 +v 11.95 0.95 1 +v 12.32 0.58 1 +v 12.41 0.98 1 +v 12.64 0.48 1 +v 12.76 0.88 1 +v 12.98 0.76 1 +v 12.87 0.26 1 +v 13.17 0.59 1 +v 13.33 0.37 1 +v 12.98 0.03 1 +v 13.45 0.12 1 +v 13.51 -0.17 1 +v 13.54 -0.63 1 +v -13.75 0.32 1 +v -13.6 0.45 1 +v -13.35 0.55 1 +v -13.62 0.85 1 +v -13.38 0.95 1 +v -12.87 0.45 1 +v -12.41 0.69 1 +v -12.76 0.27 1 +v -12.15 0.17 1 +v -12.71 0.04 1 +v -12.22 -0.12 1 +v -10.8 0.79 1 +v -11.1 0.94 1 +v -11.23 0.48 1 +v -11.67 0.95 1 +v -12.71 -1.94 1 +v -12.22 -1.94 1 +v -11.02 0.08 1 +v -10.54 0.18 1 +v -11.1 0.35 1 +v -11.44 0.56 1 +v -11.41 0.98 1 +v -10.53 -1.94 1 +v -11.01 -1.94 1 +v -10.61 0.53 1 +v -14.41 -1.94 1 +v -12.59 0.85 1 +v -13.82 0.7 1 +v -13.98 0.91 1 +v 4.33 -0.63 1 +v 3.81 -0.23 1 +v -13.93 -1.94 1 +v 3.51 -1.46 1 +v 4.31 -1.08 1 +v 3.39 -1.98 1 +v 3.7 -1.87 1 +v 3.3 -1.57 1 +v 3.05 -1.61 1 +v 1.89 -1.4 1 +v 2.25 0.01 1 +v 3.96 0.59 1 +v 3.77 0.76 1 +v 2.74 0.95 1 +v 2.77 0.55 1 +v 2.48 0.88 1 +v 2.51 0.4 1 +v 2.06 0.58 1 +v 2.36 0.24 1 +v -4.61 0.59 1 +v -4.98 0.3 1 +v -4.84 0.09 1 +v 1.89 0.36 1 +v 1.78 0.1 1 +v -6.92 -0.66 1 +v -6.32 -1.12 1 +v -6.42 -0.68 1 +v -2.86 -1.59 1 +v -2.64 -2 1 +v -2.54 -1.6 1 +v 0.62 0.31 1 +v 0.99 0.62 1 +v 0.84 0.77 1 +v 1.78 -1.15 1 +v 1.1 0.43 1 +v -6.03 -1.95 1 +v 0.61 -0.88 1 +v 0.76 -0.39 1 +v 0.38 -0.79 1 +v 2.85 -2 1 +v 16.49 -1.94 1 +v 15.89 -1.94 1 +v 2.26 0.75 1 +v 3.11 0.58 1 +v 4.23 0.12 1 +v 4.3 -0.17 1 +v -4.9 -1.24 1 +v -0.56 -1.87 1 +v -0.29 -1.97 1 +v -0.36 -1.49 1 +v -4.24 -0.28 1 +v -4.74 -0.32 1 +v -4.26 -0.88 1 +v -0.52 0.07 1 +v -0.91 -0.22 1 +v -0.78 -0.38 1 +v -6.39 -0.14 1 +v 0.95 -1.75 1 +v 0.61 -1.47 1 +v 0.72 -1.89 1 +v -5.69 0.98 1 +v -6 0.47 1 +v -5.68 0.58 1 +v 1.13 -1.56 1 +v 0.73 -1.32 1 +v 1.23 -1.33 1 +v 0.77 -1.15 1 +v 0.5 0.46 1 +v 0.62 0.88 1 +v 0.31 0.55 1 +v -0.34 -0.07 1 +v -0.53 -0.53 1 +v -4.45 0.38 1 +v -4.33 0.13 1 +v -6.71 -1.4 1 +v -6.55 -1.62 1 +v -5.58 -2 1 +v -5.75 -1.59 1 +v -4.68 -1.69 1 +v -5.16 -1.49 1 +v -5.22 -1.96 1 +v -5.47 -1.6 1 +v -6.83 -1.15 1 +v -6.89 -0.14 1 +v -6.81 0.18 1 +v -6.25 0.22 1 +v -6.67 0.44 1 +v -6.48 0.66 1 +v -6.16 0.87 1 +v -5.4 0.56 1 +v -5.13 0.92 1 +v -5.19 0.48 1 +v -4.81 0.76 1 +v -4.89 -1.83 1 +v 0.21 -1.6 1 +v 0.12 -2 1 +v 0.43 -1.98 1 +v 1.16 0.18 1 +v 0.69 0.11 1 +v -0.99 -0.04 1 +v -1.02 0.15 1 +v -0.55 0.21 1 +v -0.97 0.44 1 +v -1.52 -1.33 1 +v -2.02 -1.32 1 +v -1.63 -1.56 1 +v -1.51 -0.87 1 +v -2.02 -1 1 +v -1.98 -1.15 1 +v -0.48 0.89 1 +v -0.76 0.73 1 +v -0.39 0.49 1 +v -0.18 0.56 1 +v -0.05 0.98 1 +v 0.06 0.58 1 +v 0.34 0.96 1 +v -2.94 0.56 1 +v -2.82 0.98 1 +v -3.24 0.89 1 +v -2.69 0.58 1 +v -3.52 0.73 1 +v -3.28 0.35 1 +v -3.15 0.49 1 +v -3.31 0.21 1 +v -3.78 0.15 1 +v -3.28 0.07 1 +v -0.51 0.35 1 +v -3.29 -0.53 1 +v -2.37 -0.79 1 +v -3.1 -0.07 1 +v -1.8 -1.75 1 +v -2.14 -1.47 1 +v -2.03 -1.89 1 +v -0.54 -1.26 1 +v -0.81 -1.71 1 +v -1.02 -1.39 1 +v 0.48 -1.55 1 +v 1.24 -0.87 1 +v 1.27 -1.1 1 +v 0.73 -1 1 +v 1.02 -0.54 1 +v 1.15 -0.68 1 +v -3.12 -1.49 1 +v -3.33 -1.87 1 +v -3.05 -1.97 1 +v -0.62 -1.01 1 +v -2.32 -1.98 1 +v -2.27 -1.55 1 +v -1.1 -1.08 1 +v -1.99 -0.39 1 +v -2.14 -0.88 1 +v -0.1 -1.59 1 +v -1.74 -0.54 1 +v -2.13 0.31 1 +v -2.06 0.11 1 +v -1.65 0.43 1 +v -3.54 -0.38 1 +v -3.38 -1.01 1 +v -3.78 -1.39 1 +v -3.3 -1.26 1 +v -3.67 -0.22 1 +v -3.75 -0.04 1 +v -3.73 0.44 1 +v -2.44 0.55 1 +v -2.41 0.96 1 +v -2.13 0.88 1 +v -2.25 0.46 1 +v -1.91 0.77 1 +v -1.76 0.62 1 +v -1.59 0.18 1 +v -1.48 -1.1 1 +v -1.6 -0.68 1 +v -3.57 -1.71 1 +v -3.86 -1.08 1 +v 13.81 -1.94 1 +f 1 2 3 +f 4 5 6 +f 7 2 1 +f 8 3 9 +f 10 7 11 +f 10 2 7 +f 12 7 1 +f 13 14 15 +f 16 12 17 +f 17 12 1 +f 2 9 3 +f 9 18 8 +f 19 20 21 +f 22 19 23 +f 24 25 26 +f 27 19 28 +f 29 30 31 +f 4 32 5 +f 33 34 35 +f 36 37 38 +f 39 40 41 +f 42 37 43 +f 44 45 46 +f 47 48 49 +f 50 51 52 +f 53 52 54 +f 55 56 54 +f 57 58 59 +f 55 60 56 +f 58 61 59 +f 62 21 20 +f 54 56 53 +f 53 50 52 +f 52 51 43 +f 63 43 51 +f 42 43 63 +f 37 42 38 +f 36 38 64 +f 36 64 65 +f 66 65 67 +f 36 65 66 +f 66 67 6 +f 4 6 67 +f 49 5 47 +f 5 32 47 +f 49 48 68 +f 31 68 48 +f 31 69 68 +f 31 70 69 +f 71 72 73 +f 22 74 28 +f 19 22 28 +f 21 23 19 +f 75 21 62 +f 76 75 62 +f 60 55 75 +f 60 75 76 +f 77 78 79 +f 80 81 82 +f 83 34 84 +f 85 86 87 +f 88 89 90 +f 33 91 34 +f 92 93 94 +f 24 95 96 +f 97 98 99 +f 100 101 102 +f 102 101 103 +f 94 93 97 +f 104 105 44 +f 92 106 93 +f 107 108 109 +f 110 111 112 +f 25 24 113 +f 114 115 87 +f 116 117 118 +f 119 120 121 +f 122 121 123 +f 124 115 114 +f 87 115 85 +f 125 124 114 +f 78 90 126 +f 120 87 86 +f 121 120 86 +f 122 119 121 +f 127 15 128 +f 24 96 113 +f 24 129 95 +f 130 129 99 +f 95 129 130 +f 98 130 99 +f 93 98 97 +f 92 131 106 +f 106 131 132 +f 88 106 132 +f 89 88 132 +f 133 90 89 +f 126 90 133 +f 134 78 126 +f 79 78 134 +f 135 77 79 +f 135 125 77 +f 135 124 125 +f 71 136 72 +f 137 138 139 +f 140 141 142 +f 71 73 143 +f 144 145 146 +f 147 148 149 +f 113 150 122 +f 151 152 153 +f 152 154 155 +f 153 152 156 +f 156 152 155 +f 152 151 157 +f 150 119 122 +f 156 144 148 +f 153 156 148 +f 153 148 147 +f 156 145 144 +f 25 113 122 +f 158 146 145 +f 143 158 145 +f 159 158 143 +f 143 73 159 +f 160 161 162 +f 163 164 165 +f 165 139 163 +f 166 167 168 +f 139 138 163 +f 166 169 142 +f 169 140 142 +f 170 169 168 +f 162 171 160 +f 172 170 168 +f 170 173 174 +f 172 175 170 +f 169 170 174 +f 176 167 166 +f 177 178 179 +f 178 177 180 +f 169 166 168 +f 140 181 141 +f 182 183 184 +f 140 138 181 +f 185 182 184 +f 138 137 181 +f 186 177 179 +f 187 188 189 +f 190 178 180 +f 191 160 186 +f 192 193 194 +f 191 186 179 +f 195 196 197 +f 171 186 160 +f 198 194 193 +f 199 200 193 +f 201 202 200 +f 203 197 202 +f 204 182 205 +f 206 117 116 +f 207 208 209 +f 207 195 208 +f 197 208 195 +f 197 196 202 +f 203 202 201 +f 199 201 200 +f 192 199 193 +f 194 198 161 +f 162 161 198 +f 210 196 195 +f 209 211 207 +f 212 211 213 +f 207 211 212 +f 214 213 215 +f 212 213 214 +f 216 214 217 +f 214 215 217 +f 216 217 218 +f 219 216 220 +f 216 218 220 +f 221 220 222 +f 219 220 221 +f 222 223 221 +f 224 223 225 +f 221 223 224 +f 225 226 224 +f 224 226 227 +f 227 226 228 +f 210 227 228 +f 229 210 228 +f 230 117 231 +f 232 231 233 +f 234 232 233 +f 111 110 235 +f 127 236 237 +f 238 239 240 +f 241 242 243 +f 244 14 13 +f 239 245 240 +f 196 210 229 +f 246 245 239 +f 247 248 249 +f 236 238 240 +f 127 238 236 +f 14 128 15 +f 244 250 251 +f 14 244 251 +f 242 251 243 +f 252 247 249 +f 247 252 253 +f 248 254 249 +f 206 116 255 +f 248 241 254 +f 242 241 248 +f 250 243 251 +f 256 237 235 +f 237 236 235 +f 235 110 256 +f 128 238 127 +f 232 112 111 +f 232 234 112 +f 233 231 257 +f 258 117 206 +f 231 117 257 +f 230 118 117 +f 102 259 260 +f 261 255 116 +f 57 262 58 +f 61 263 59 +f 264 262 265 +f 262 57 265 +f 264 266 262 +f 266 264 267 +f 100 91 268 +f 103 269 270 +f 103 101 269 +f 107 109 271 +f 271 109 272 +f 272 81 80 +f 273 82 274 +f 275 44 105 +f 276 274 277 +f 278 277 279 +f 280 270 281 +f 282 283 284 +f 281 270 269 +f 285 286 287 +f 288 289 290 +f 291 101 100 +f 292 288 290 +f 293 294 41 +f 291 100 268 +f 268 91 33 +f 294 39 41 +f 83 35 34 +f 295 296 297 +f 298 83 84 +f 267 298 84 +f 264 298 267 +f 299 300 187 +f 280 279 270 +f 102 103 259 +f 280 278 279 +f 278 301 277 +f 276 277 301 +f 274 276 273 +f 274 82 302 +f 81 302 82 +f 109 81 272 +f 107 303 108 +f 108 303 259 +f 259 303 260 +f 299 187 185 +f 304 305 306 +f 307 46 45 +f 308 309 310 +f 311 312 313 +f 314 315 316 +f 317 308 318 +f 282 284 319 +f 320 314 321 +f 322 323 324 +f 321 325 326 +f 327 328 329 +f 318 296 317 +f 308 317 309 +f 330 308 331 +f 332 333 334 +f 333 307 335 +f 336 40 337 +f 293 338 339 +f 293 41 338 +f 340 341 275 +f 342 343 341 +f 39 337 40 +f 344 40 345 +f 40 336 345 +f 346 344 345 +f 347 344 346 +f 346 348 347 +f 324 348 349 +f 347 348 324 +f 324 349 322 +f 322 350 323 +f 319 350 351 +f 323 350 319 +f 282 319 351 +f 283 282 313 +f 312 283 313 +f 312 311 352 +f 334 352 353 +f 312 352 334 +f 353 332 334 +f 334 333 335 +f 335 307 45 +f 46 104 44 +f 105 340 275 +f 341 340 354 +f 342 341 354 +f 339 343 342 +f 339 338 343 +f 355 356 292 +f 331 308 310 +f 289 327 329 +f 330 331 357 +f 358 359 360 +f 361 362 363 +f 364 365 366 +f 365 364 367 +f 367 364 368 +f 328 368 369 +f 288 292 356 +f 327 289 288 +f 328 367 368 +f 329 328 369 +f 370 371 372 +f 370 372 373 +f 374 375 376 +f 365 377 366 +f 378 379 380 +f 377 357 366 +f 357 377 330 +f 381 382 383 +f 305 384 385 +f 384 386 385 +f 387 388 389 +f 297 296 318 +f 390 391 325 +f 295 392 393 +f 297 392 295 +f 392 394 393 +f 325 393 390 +f 393 394 390 +f 325 391 326 +f 320 321 326 +f 314 316 321 +f 315 388 316 +f 389 388 315 +f 395 396 397 +f 398 385 386 +f 286 399 287 +f 386 400 398 +f 401 402 380 +f 384 305 304 +f 306 305 403 +f 389 306 403 +f 387 389 403 +f 363 402 404 +f 360 362 358 +f 405 406 407 +f 379 378 408 +f 408 378 409 +f 410 411 412 +f 413 408 409 +f 408 413 414 +f 415 376 416 +f 371 375 374 +f 371 370 375 +f 373 417 418 +f 373 372 417 +f 419 418 420 +f 420 421 405 +f 407 406 422 +f 421 406 405 +f 420 405 419 +f 373 418 419 +f 376 415 374 +f 414 415 416 +f 416 408 414 +f 401 380 379 +f 361 423 362 +f 402 401 404 +f 383 382 399 +f 363 404 424 +f 361 363 424 +f 362 423 358 +f 360 359 381 +f 381 383 360 +f 287 399 382 +f 395 397 425 +f 412 411 425 +f 412 426 410 +f 425 411 395 +f 395 285 396 +f 396 285 287 +f 182 185 189 +f 205 182 189 +f 189 188 427 +f 187 189 185 +f 28 74 70 +f 28 70 30 +f 70 31 30 +f 428 429 430 +f 431 432 433 +f 434 428 430 +f 435 436 429 +f 437 438 434 +f 437 434 430 +f 439 428 434 +f 440 441 442 +f 443 444 439 +f 444 428 439 +f 430 429 436 +f 436 435 445 +f 446 447 448 +f 449 450 451 +f 452 453 446 +f 454 455 456 +f 431 433 457 +f 458 459 460 +f 461 462 463 +f 464 465 466 +f 466 467 468 +f 469 470 471 +f 472 473 468 +f 474 475 476 +f 477 473 478 +f 479 480 481 +f 482 480 483 +f 484 485 486 +f 482 483 487 +f 486 485 488 +f 489 448 447 +f 480 479 483 +f 479 481 477 +f 481 473 477 +f 472 478 473 +f 467 472 468 +f 466 490 467 +f 466 465 490 +f 464 491 465 +f 464 492 491 +f 464 432 492 +f 431 492 432 +f 475 474 433 +f 433 474 457 +f 475 493 476 +f 455 476 493 +f 455 493 494 +f 455 494 495 +f 496 497 498 +f 499 453 500 +f 446 453 499 +f 446 499 501 +f 447 446 501 +f 502 489 447 +f 503 489 502 +f 487 502 482 +f 487 503 502 +f 504 505 506 +f 507 508 509 +f 510 511 460 +f 512 513 514 +f 458 460 515 +f 516 517 518 +f 449 519 520 +f 521 522 523 +f 524 525 526 +f 525 527 528 +f 517 521 518 +f 529 530 531 +f 516 518 532 +f 533 534 535 +f 536 537 538 +f 451 539 449 +f 540 541 542 +f 449 539 543 +f 544 545 546 +f 547 548 549 +f 550 540 542 +f 541 512 542 +f 541 513 512 +f 551 540 552 +f 463 509 553 +f 549 514 513 +f 549 513 554 +f 547 549 554 +f 547 554 555 +f 556 557 441 +f 449 543 519 +f 449 520 558 +f 559 522 558 +f 520 559 558 +f 523 522 559 +f 518 521 523 +f 516 532 560 +f 462 561 560 +f 532 462 560 +f 461 561 462 +f 553 461 463 +f 508 553 509 +f 562 508 507 +f 563 562 507 +f 563 507 551 +f 563 551 552 +f 552 540 550 +f 496 498 564 +f 565 566 567 +f 568 569 570 +f 496 571 497 +f 572 573 574 +f 575 576 577 +f 578 579 580 +f 580 581 582 +f 579 583 580 +f 583 581 580 +f 580 584 578 +f 539 547 555 +f 583 577 572 +f 579 577 583 +f 579 575 577 +f 583 572 574 +f 451 547 539 +f 585 574 573 +f 571 574 585 +f 586 571 585 +f 571 586 497 +f 587 588 589 +f 590 591 592 +f 591 590 566 +f 593 594 595 +f 566 590 567 +f 593 569 596 +f 596 569 568 +f 597 594 596 +f 588 587 598 +f 599 594 597 +f 597 600 601 +f 599 597 602 +f 596 600 597 +f 603 593 595 +f 604 605 606 +f 606 607 604 +f 596 594 593 +f 568 570 608 +f 609 610 611 +f 568 608 567 +f 612 610 609 +f 567 608 565 +f 613 605 604 +f 614 615 616 +f 617 607 606 +f 618 613 587 +f 619 620 621 +f 618 605 613 +f 622 623 624 +f 598 587 613 +f 625 621 620 +f 626 621 627 +f 628 627 629 +f 630 629 631 +f 623 631 624 +f 632 633 609 +f 634 544 546 +f 635 636 623 +f 635 623 622 +f 631 629 624 +f 630 628 629 +f 626 627 628 +f 619 621 626 +f 620 589 625 +f 588 625 589 +f 637 622 624 +f 636 635 638 +f 639 640 638 +f 635 639 638 +f 641 642 640 +f 639 641 640 +f 643 644 641 +f 641 644 642 +f 643 645 644 +f 646 647 643 +f 643 647 645 +f 648 649 647 +f 646 648 647 +f 649 648 650 +f 651 652 650 +f 648 651 650 +f 652 651 653 +f 651 654 653 +f 654 655 653 +f 654 656 655 +f 637 656 654 +f 657 656 637 +f 658 659 546 +f 660 661 659 +f 662 661 660 +f 538 663 536 +f 556 664 665 +f 666 667 668 +f 669 670 671 +f 672 440 442 +f 668 667 673 +f 624 657 637 +f 674 668 673 +f 675 676 677 +f 665 667 666 +f 556 665 666 +f 442 441 557 +f 672 678 679 +f 442 678 672 +f 671 670 678 +f 680 676 675 +f 675 681 680 +f 677 676 682 +f 634 683 544 +f 677 682 669 +f 671 677 669 +f 679 678 670 +f 684 663 664 +f 664 663 665 +f 663 684 536 +f 557 556 666 +f 660 538 537 +f 660 537 662 +f 661 685 659 +f 686 634 546 +f 659 685 546 +f 658 546 545 +f 525 687 688 +f 689 544 683 +f 484 486 690 +f 488 485 691 +f 692 693 690 +f 690 693 484 +f 692 690 694 +f 694 695 692 +f 524 696 515 +f 527 697 528 +f 533 698 534 +f 698 699 534 +f 699 504 506 +f 700 701 505 +f 702 703 701 +f 704 705 703 +f 706 707 708 +f 709 710 697 +f 711 712 713 +f 710 528 697 +f 714 715 716 +f 526 525 528 +f 717 718 719 +f 720 524 526 +f 721 718 717 +f 720 696 524 +f 696 458 515 +f 722 470 469 +f 510 460 459 +f 723 724 725 +f 726 511 510 +f 695 511 726 +f 692 695 726 +f 727 614 728 +f 709 697 705 +f 525 688 527 +f 709 705 704 +f 704 703 729 +f 702 729 703 +f 701 700 702 +f 701 730 505 +f 506 505 730 +f 534 699 506 +f 533 535 731 +f 731 535 732 +f 535 688 732 +f 688 687 732 +f 727 612 614 +f 531 530 733 +f 734 735 736 +f 737 738 739 +f 740 741 742 +f 713 743 711 +f 744 745 746 +f 747 748 749 +f 750 751 744 +f 751 752 753 +f 754 755 756 +f 757 758 725 +f 757 742 758 +f 759 708 760 +f 760 708 737 +f 761 762 712 +f 722 763 764 +f 722 764 470 +f 765 733 766 +f 767 766 768 +f 469 471 762 +f 712 762 471 +f 712 769 761 +f 711 769 712 +f 711 743 770 +f 743 771 770 +f 772 773 771 +f 743 772 771 +f 773 772 774 +f 748 775 774 +f 772 748 774 +f 747 775 748 +f 776 777 747 +f 749 776 747 +f 778 777 776 +f 778 779 777 +f 707 706 779 +f 778 707 779 +f 706 708 759 +f 708 738 737 +f 738 530 739 +f 739 530 529 +f 531 733 765 +f 766 780 765 +f 767 780 766 +f 763 767 768 +f 763 768 764 +f 781 782 783 +f 784 721 785 +f 786 741 740 +f 787 786 740 +f 719 755 754 +f 788 789 787 +f 790 791 792 +f 793 794 795 +f 796 797 798 +f 798 799 796 +f 799 800 796 +f 801 802 800 +f 756 802 801 +f 717 785 721 +f 754 717 719 +f 801 800 799 +f 755 802 756 +f 803 804 805 +f 803 806 804 +f 807 808 809 +f 810 811 812 +f 798 797 813 +f 814 815 816 +f 813 797 789 +f 789 788 813 +f 740 788 787 +f 817 818 819 +f 757 740 742 +f 736 820 821 +f 821 820 822 +f 781 783 823 +f 724 757 725 +f 824 753 825 +f 723 826 827 +f 724 723 827 +f 827 826 828 +f 753 824 826 +f 826 824 828 +f 753 752 825 +f 750 752 751 +f 744 751 745 +f 746 745 823 +f 783 746 823 +f 829 830 831 +f 832 822 820 +f 716 833 834 +f 822 832 835 +f 836 815 837 +f 821 734 736 +f 735 838 736 +f 782 838 735 +f 781 838 782 +f 794 839 837 +f 791 790 795 +f 840 841 842 +f 816 843 814 +f 844 845 846 +f 847 843 812 +f 812 848 847 +f 849 810 808 +f 805 807 809 +f 805 809 803 +f 806 850 851 +f 850 852 851 +f 806 851 804 +f 853 854 852 +f 854 840 855 +f 841 856 842 +f 855 840 842 +f 854 853 840 +f 850 853 852 +f 808 807 849 +f 811 810 849 +f 848 812 811 +f 843 816 812 +f 836 816 815 +f 793 795 857 +f 837 839 836 +f 818 834 819 +f 794 858 839 +f 793 858 794 +f 795 790 857 +f 791 817 792 +f 817 791 818 +f 833 819 834 +f 829 859 830 +f 845 859 846 +f 845 844 860 +f 859 829 846 +f 829 831 714 +f 831 715 714 +f 715 833 716 +f 609 615 612 +f 633 615 609 +f 615 861 616 +f 614 612 615 +f 453 495 500 +f 453 456 495 +f 495 456 455 +f 18 445 8 +f 435 8 445 +f 9 436 18 +f 445 18 436 +f 2 430 9 +f 436 9 430 +f 10 437 2 +f 430 2 437 +f 11 438 10 +f 437 10 438 +f 7 434 11 +f 438 11 434 +f 12 439 7 +f 434 7 439 +f 16 443 12 +f 439 12 443 +f 17 444 16 +f 443 16 444 +f 1 428 17 +f 444 17 428 +f 3 429 1 +f 428 1 429 +f 8 435 3 +f 429 3 435 +f 76 503 60 +f 487 60 503 +f 62 489 76 +f 503 76 489 +f 20 448 62 +f 489 62 448 +f 19 446 20 +f 448 20 446 +f 27 452 19 +f 446 19 452 +f 28 453 27 +f 452 27 453 +f 30 456 28 +f 453 28 456 +f 29 454 30 +f 456 30 454 +f 31 455 29 +f 454 29 455 +f 48 476 31 +f 455 31 476 +f 47 474 48 +f 476 48 474 +f 32 457 47 +f 474 47 457 +f 4 431 32 +f 457 32 431 +f 67 492 4 +f 431 4 492 +f 65 491 67 +f 492 67 491 +f 64 465 65 +f 491 65 465 +f 38 490 64 +f 465 64 490 +f 42 467 38 +f 490 38 467 +f 63 472 42 +f 467 42 472 +f 51 478 63 +f 472 63 478 +f 50 477 51 +f 478 51 477 +f 53 479 50 +f 477 50 479 +f 56 483 53 +f 479 53 483 +f 60 487 56 +f 483 56 487 +f 55 482 75 +f 502 75 482 +f 54 480 55 +f 482 55 480 +f 52 481 54 +f 480 54 481 +f 481 52 473 +f 43 473 52 +f 473 43 468 +f 37 468 43 +f 468 37 466 +f 36 466 37 +f 466 36 464 +f 66 464 36 +f 6 432 66 +f 464 66 432 +f 5 433 6 +f 432 6 433 +f 49 475 5 +f 433 5 475 +f 68 493 49 +f 475 49 493 +f 69 494 68 +f 493 68 494 +f 70 495 69 +f 494 69 495 +f 74 500 70 +f 495 70 500 +f 22 499 74 +f 500 74 499 +f 499 22 501 +f 23 501 22 +f 21 447 23 +f 501 23 447 +f 75 502 21 +f 447 21 502 +f 550 124 552 +f 135 563 124 +f 552 124 563 +f 79 562 135 +f 563 135 562 +f 134 508 79 +f 562 79 508 +f 126 553 134 +f 508 134 553 +f 133 553 126 +f 89 461 133 +f 553 133 461 +f 132 561 89 +f 461 89 561 +f 131 560 132 +f 561 132 560 +f 92 516 131 +f 560 131 516 +f 94 517 92 +f 516 92 517 +f 97 521 94 +f 517 94 521 +f 99 522 97 +f 521 97 522 +f 129 558 99 +f 522 99 558 +f 24 449 129 +f 558 129 449 +f 26 450 24 +f 449 24 450 +f 25 451 26 +f 450 26 451 +f 122 547 25 +f 451 25 547 +f 123 548 122 +f 547 122 548 +f 121 549 123 +f 548 123 549 +f 86 514 121 +f 549 121 514 +f 85 512 86 +f 514 86 512 +f 115 542 85 +f 512 85 542 +f 124 550 115 +f 542 115 550 +f 87 541 114 +f 540 114 541 +f 541 87 513 +f 120 513 87 +f 513 120 554 +f 119 554 120 +f 554 119 555 +f 150 555 119 +f 555 150 539 +f 113 539 150 +f 539 113 543 +f 96 519 113 +f 543 113 519 +f 95 520 96 +f 519 96 520 +f 130 559 95 +f 520 95 559 +f 98 523 130 +f 559 130 523 +f 93 518 98 +f 523 98 518 +f 106 532 93 +f 518 93 532 +f 532 106 462 +f 88 462 106 +f 462 88 463 +f 90 463 88 +f 463 90 509 +f 78 509 90 +f 509 78 507 +f 77 507 78 +f 125 551 77 +f 507 77 551 +f 114 540 125 +f 551 125 540 +f 72 498 73 +f 497 73 498 +f 136 564 72 +f 498 72 564 +f 71 496 136 +f 564 136 496 +f 143 571 71 +f 496 71 571 +f 145 574 143 +f 571 143 574 +f 156 583 145 +f 574 145 583 +f 155 581 156 +f 583 156 581 +f 154 582 155 +f 581 155 582 +f 152 580 154 +f 582 154 580 +f 157 584 152 +f 580 152 584 +f 151 578 157 +f 584 157 578 +f 153 579 151 +f 578 151 579 +f 147 575 153 +f 579 153 575 +f 149 576 147 +f 575 147 576 +f 148 577 149 +f 576 149 577 +f 144 572 148 +f 577 148 572 +f 146 573 144 +f 572 144 573 +f 158 585 146 +f 573 146 585 +f 159 586 158 +f 585 158 586 +f 73 497 159 +f 586 159 497 +f 165 591 139 +f 566 139 591 +f 164 592 165 +f 591 165 592 +f 163 590 164 +f 592 164 590 +f 138 567 163 +f 590 163 567 +f 140 568 138 +f 567 138 568 +f 169 596 140 +f 568 140 596 +f 174 600 169 +f 596 169 600 +f 173 601 174 +f 600 174 601 +f 170 597 173 +f 601 173 597 +f 175 602 170 +f 597 170 602 +f 172 599 175 +f 602 175 599 +f 168 594 172 +f 599 172 594 +f 167 595 168 +f 594 168 595 +f 176 603 167 +f 595 167 603 +f 166 593 176 +f 603 176 593 +f 142 569 166 +f 593 166 569 +f 141 570 142 +f 569 142 570 +f 181 608 141 +f 570 141 608 +f 137 565 181 +f 608 181 565 +f 139 566 137 +f 565 137 566 +f 160 587 161 +f 589 161 587 +f 191 618 160 +f 587 160 618 +f 179 605 191 +f 618 191 605 +f 178 606 179 +f 605 179 606 +f 190 617 178 +f 606 178 617 +f 180 607 190 +f 617 190 607 +f 177 604 180 +f 607 180 604 +f 186 613 177 +f 604 177 613 +f 171 598 186 +f 613 186 598 +f 162 588 171 +f 598 171 588 +f 198 625 162 +f 588 162 625 +f 193 621 198 +f 625 198 621 +f 200 627 193 +f 621 193 627 +f 202 629 200 +f 627 200 629 +f 196 624 202 +f 629 202 624 +f 229 657 196 +f 624 196 657 +f 228 656 229 +f 657 229 656 +f 656 228 655 +f 226 653 228 +f 655 228 653 +f 225 652 226 +f 653 226 652 +f 223 650 225 +f 652 225 650 +f 222 649 223 +f 650 223 649 +f 220 647 222 +f 649 222 647 +f 218 645 220 +f 647 220 645 +f 217 644 218 +f 645 218 644 +f 215 642 217 +f 644 217 642 +f 213 640 215 +f 642 215 640 +f 211 638 213 +f 640 213 638 +f 209 636 211 +f 638 211 636 +f 208 623 209 +f 636 209 623 +f 197 623 208 +f 623 197 631 +f 203 630 197 +f 631 197 630 +f 201 628 203 +f 630 203 628 +f 199 626 201 +f 628 201 626 +f 192 619 199 +f 626 199 619 +f 194 620 192 +f 619 192 620 +f 161 589 194 +f 620 194 589 +f 195 622 210 +f 637 210 622 +f 207 635 195 +f 622 195 635 +f 212 639 207 +f 635 207 639 +f 214 641 212 +f 639 212 641 +f 216 643 214 +f 641 214 643 +f 219 646 216 +f 643 216 646 +f 221 648 219 +f 646 219 648 +f 224 651 221 +f 648 221 651 +f 227 654 224 +f 651 224 654 +f 210 637 227 +f 654 227 637 +f 116 544 261 +f 689 261 544 +f 118 545 116 +f 544 116 545 +f 230 658 118 +f 545 118 658 +f 231 659 230 +f 658 230 659 +f 232 660 231 +f 659 231 660 +f 111 538 232 +f 660 232 538 +f 235 663 111 +f 538 111 663 +f 236 665 235 +f 663 235 665 +f 240 667 236 +f 665 236 667 +f 245 673 240 +f 667 240 673 +f 246 674 245 +f 673 245 674 +f 239 668 246 +f 674 246 668 +f 238 666 239 +f 668 239 666 +f 128 557 238 +f 666 238 557 +f 14 442 128 +f 557 128 442 +f 251 678 14 +f 442 14 678 +f 242 671 251 +f 678 251 671 +f 248 677 242 +f 671 242 677 +f 247 675 248 +f 677 248 675 +f 253 681 247 +f 675 247 681 +f 252 680 253 +f 681 253 680 +f 249 676 252 +f 680 252 676 +f 254 682 249 +f 676 249 682 +f 241 669 254 +f 682 254 669 +f 243 670 241 +f 669 241 670 +f 250 679 243 +f 670 243 679 +f 244 672 250 +f 679 250 672 +f 13 440 244 +f 672 244 440 +f 15 441 13 +f 440 13 441 +f 127 556 15 +f 441 15 556 +f 237 664 127 +f 556 127 664 +f 256 684 237 +f 664 237 684 +f 110 536 256 +f 684 256 536 +f 112 537 110 +f 536 110 537 +f 234 662 112 +f 537 112 662 +f 233 661 234 +f 662 234 661 +f 257 685 233 +f 661 233 685 +f 117 546 257 +f 685 257 546 +f 258 686 117 +f 546 117 686 +f 206 634 258 +f 686 258 634 +f 255 683 206 +f 634 206 683 +f 261 689 255 +f 683 255 689 +f 264 692 298 +f 726 298 692 +f 265 693 264 +f 692 264 693 +f 57 484 265 +f 693 265 484 +f 59 485 57 +f 484 57 485 +f 263 691 59 +f 485 59 691 +f 61 488 263 +f 691 263 488 +f 58 486 61 +f 488 61 486 +f 262 690 58 +f 486 58 690 +f 266 694 262 +f 690 262 694 +f 267 695 266 +f 694 266 695 +f 84 511 267 +f 695 267 511 +f 34 460 84 +f 511 84 460 +f 91 515 34 +f 460 34 515 +f 100 524 91 +f 515 91 524 +f 102 525 100 +f 524 100 525 +f 260 687 102 +f 525 102 687 +f 303 732 260 +f 687 260 732 +f 732 303 731 +f 107 533 303 +f 731 303 533 +f 271 698 107 +f 533 107 698 +f 272 699 271 +f 698 271 699 +f 80 504 272 +f 699 272 504 +f 82 505 80 +f 504 80 505 +f 273 700 82 +f 505 82 700 +f 276 702 273 +f 700 273 702 +f 301 729 276 +f 702 276 729 +f 278 704 301 +f 729 301 704 +f 280 709 278 +f 704 278 709 +f 281 710 280 +f 709 280 710 +f 269 528 281 +f 710 281 528 +f 101 528 269 +f 528 101 526 +f 291 720 101 +f 526 101 720 +f 268 696 291 +f 720 291 696 +f 33 458 268 +f 696 268 458 +f 35 459 33 +f 458 33 459 +f 83 510 35 +f 459 35 510 +f 298 726 83 +f 510 83 726 +f 103 527 259 +f 688 259 527 +f 270 697 103 +f 527 103 697 +f 279 705 270 +f 697 270 705 +f 277 703 279 +f 705 279 703 +f 274 701 277 +f 703 277 701 +f 302 730 274 +f 701 274 730 +f 81 506 302 +f 730 302 506 +f 109 534 81 +f 506 81 534 +f 108 535 109 +f 534 109 535 +f 259 688 108 +f 535 108 688 +f 342 767 339 +f 763 339 767 +f 354 780 342 +f 767 342 780 +f 340 765 354 +f 780 354 765 +f 105 531 340 +f 765 340 531 +f 104 529 105 +f 531 105 529 +f 46 739 104 +f 529 104 739 +f 307 737 46 +f 739 46 737 +f 333 760 307 +f 737 307 760 +f 332 759 333 +f 760 333 759 +f 353 706 332 +f 759 332 706 +f 352 779 353 +f 706 353 779 +f 311 777 352 +f 779 352 777 +f 313 777 311 +f 282 747 313 +f 777 313 747 +f 351 775 282 +f 747 282 775 +f 350 774 351 +f 775 351 774 +f 322 773 350 +f 774 350 773 +f 349 771 322 +f 773 322 771 +f 348 770 349 +f 771 349 770 +f 346 711 348 +f 770 348 711 +f 345 769 346 +f 711 346 769 +f 336 761 345 +f 769 345 761 +f 337 762 336 +f 761 336 762 +f 39 469 337 +f 762 337 469 +f 294 722 39 +f 469 39 722 +f 293 722 294 +f 339 763 293 +f 722 293 763 +f 768 338 764 +f 41 470 338 +f 764 338 470 +f 40 471 41 +f 470 41 471 +f 471 40 712 +f 344 712 40 +f 712 344 713 +f 347 713 344 +f 713 347 743 +f 324 743 347 +f 743 324 772 +f 323 772 324 +f 319 748 323 +f 772 323 748 +f 284 749 319 +f 748 319 749 +f 283 749 284 +f 749 283 776 +f 312 778 283 +f 776 283 778 +f 334 707 312 +f 778 312 707 +f 707 334 708 +f 335 708 334 +f 708 335 738 +f 45 738 335 +f 738 45 530 +f 44 530 45 +f 530 44 733 +f 275 733 44 +f 341 766 275 +f 733 275 766 +f 343 768 341 +f 766 341 768 +f 338 768 343 +f 782 389 783 +f 315 746 389 +f 783 389 746 +f 314 744 315 +f 746 315 744 +f 320 750 314 +f 744 314 750 +f 326 752 320 +f 750 320 752 +f 391 825 326 +f 752 326 825 +f 390 824 391 +f 825 391 824 +f 394 828 390 +f 824 390 828 +f 392 827 394 +f 828 394 827 +f 297 724 392 +f 827 392 724 +f 318 757 297 +f 724 297 757 +f 308 757 318 +f 757 308 740 +f 330 740 308 +f 740 330 788 +f 377 813 330 +f 788 330 813 +f 365 798 377 +f 813 377 798 +f 367 799 365 +f 798 365 799 +f 328 801 367 +f 799 367 801 +f 801 328 756 +f 327 754 328 +f 756 328 754 +f 288 717 327 +f 754 327 717 +f 356 785 288 +f 717 288 785 +f 355 784 356 +f 785 356 784 +f 292 721 355 +f 784 355 721 +f 290 718 292 +f 721 292 718 +f 289 719 290 +f 718 290 719 +f 329 755 289 +f 719 289 755 +f 369 802 329 +f 755 329 802 +f 368 800 369 +f 802 369 800 +f 364 796 368 +f 800 368 796 +f 366 797 364 +f 796 364 797 +f 357 789 366 +f 797 366 789 +f 331 787 357 +f 789 357 787 +f 787 331 786 +f 310 741 331 +f 786 331 741 +f 309 742 310 +f 741 310 742 +f 317 758 309 +f 742 309 758 +f 296 725 317 +f 758 317 725 +f 295 723 296 +f 725 296 723 +f 393 826 295 +f 723 295 826 +f 325 753 393 +f 826 393 753 +f 321 751 325 +f 753 325 751 +f 316 745 321 +f 751 321 745 +f 388 823 316 +f 745 316 823 +f 387 781 388 +f 823 388 781 +f 403 838 387 +f 781 387 838 +f 305 736 403 +f 838 403 736 +f 385 820 305 +f 736 305 820 +f 398 832 385 +f 820 385 832 +f 400 835 398 +f 832 398 835 +f 386 822 400 +f 835 400 822 +f 384 821 386 +f 822 386 821 +f 304 734 384 +f 821 384 734 +f 306 735 304 +f 734 304 735 +f 389 782 306 +f 735 306 782 +f 715 287 833 +f 382 819 287 +f 833 287 819 +f 381 817 382 +f 819 382 817 +f 359 792 381 +f 817 381 792 +f 358 790 359 +f 792 359 790 +f 423 857 358 +f 790 358 857 +f 361 793 423 +f 857 423 793 +f 424 858 361 +f 793 361 858 +f 404 839 424 +f 858 424 839 +f 401 836 404 +f 839 404 836 +f 379 816 401 +f 836 401 816 +f 408 816 379 +f 816 408 812 +f 416 812 408 +f 812 416 810 +f 376 808 416 +f 810 416 808 +f 375 809 376 +f 808 376 809 +f 370 803 375 +f 809 375 803 +f 373 806 370 +f 803 370 806 +f 806 373 850 +f 419 853 373 +f 850 373 853 +f 405 840 419 +f 853 419 840 +f 407 841 405 +f 840 405 841 +f 422 856 407 +f 841 407 856 +f 406 842 422 +f 856 422 842 +f 421 855 406 +f 842 406 855 +f 420 854 421 +f 855 421 854 +f 418 852 420 +f 854 420 852 +f 417 851 418 +f 852 418 851 +f 372 804 417 +f 851 417 804 +f 371 805 372 +f 804 372 805 +f 374 807 371 +f 805 371 807 +f 415 849 374 +f 807 374 849 +f 414 811 415 +f 849 415 811 +f 811 414 848 +f 413 847 414 +f 848 414 847 +f 409 843 413 +f 847 413 843 +f 378 814 409 +f 843 409 814 +f 380 815 378 +f 814 378 815 +f 402 837 380 +f 815 380 837 +f 363 794 402 +f 837 402 794 +f 362 795 363 +f 794 363 795 +f 360 791 362 +f 795 362 791 +f 383 818 360 +f 791 360 818 +f 399 834 383 +f 818 383 834 +f 286 716 399 +f 834 399 716 +f 285 714 286 +f 716 286 714 +f 395 829 285 +f 714 285 829 +f 411 846 395 +f 829 395 846 +f 410 844 411 +f 846 411 844 +f 426 860 410 +f 844 410 860 +f 412 845 426 +f 860 426 845 +f 425 859 412 +f 845 412 859 +f 397 830 425 +f 859 425 830 +f 396 831 397 +f 830 397 831 +f 287 715 396 +f 831 396 715 +f 187 614 188 +f 616 188 614 +f 300 728 187 +f 614 187 728 +f 299 727 300 +f 728 300 727 +f 185 612 299 +f 727 299 612 +f 184 610 185 +f 612 185 610 +f 183 611 184 +f 610 184 611 +f 182 609 183 +f 611 183 609 +f 204 632 182 +f 609 182 632 +f 205 633 204 +f 632 204 633 +f 189 615 205 +f 633 205 615 +f 427 861 189 +f 615 189 861 +f 188 616 427 +f 861 427 616 diff --git a/resources/icons/add_text_modifier.svg b/resources/icons/add_text_modifier.svg new file mode 100644 index 0000000000..0a8376741f --- /dev/null +++ b/resources/icons/add_text_modifier.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/add_text_negative.svg b/resources/icons/add_text_negative.svg new file mode 100644 index 0000000000..866dd076e0 --- /dev/null +++ b/resources/icons/add_text_negative.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/add_text_part.svg b/resources/icons/add_text_part.svg new file mode 100644 index 0000000000..4a75a5d833 --- /dev/null +++ b/resources/icons/add_text_part.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/make_bold.svg b/resources/icons/make_bold.svg new file mode 100644 index 0000000000..7d8eb226c1 --- /dev/null +++ b/resources/icons/make_bold.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/make_italic.svg b/resources/icons/make_italic.svg new file mode 100644 index 0000000000..c91ee041a7 --- /dev/null +++ b/resources/icons/make_italic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/make_unbold.svg b/resources/icons/make_unbold.svg new file mode 100644 index 0000000000..41f92d1561 --- /dev/null +++ b/resources/icons/make_unbold.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/make_unitalic.svg b/resources/icons/make_unitalic.svg new file mode 100644 index 0000000000..9cefaa6383 --- /dev/null +++ b/resources/icons/make_unitalic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/search.svg b/resources/icons/search.svg index 6421c7e055..c6cdd007e4 100644 --- a/resources/icons/search.svg +++ b/resources/icons/search.svg @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/src/admesh/shared.cpp b/src/admesh/shared.cpp index a8fbc2a87d..807d9ef4c4 100644 --- a/src/admesh/shared.cpp +++ b/src/admesh/shared.cpp @@ -207,6 +207,30 @@ bool its_write_obj(const indexed_triangle_set &its, const char *file) return true; } +bool its_write_obj(const indexed_triangle_set& its, const std::vector &color, const char* file) +{ + Slic3r::CNumericLocalesSetter locales_setter; + FILE* fp = fopen(file, "w"); + if (fp == nullptr) { + return false; + } + + for (size_t i = 0; i < its.vertices.size(); ++i) + fprintf(fp, "v %f %f %f %f %f %f\n", + its.vertices[i](0), + its.vertices[i](1), + its.vertices[i](2), + color[i](0), + color[i](1), + color[i](2)); + for (size_t i = 0; i < its.indices.size(); ++i) + fprintf(fp, "f %d %d %d\n", + its.indices[i][0] + 1, + its.indices[i][1] + 1, + its.indices[i][2] + 1); + fclose(fp); + return true; +} // Check validity of the mesh, assert on error. bool stl_validate(const stl_file *stl, const indexed_triangle_set &its) diff --git a/src/admesh/stl.h b/src/admesh/stl.h index 8c30a6ae5d..ac51ae1b68 100644 --- a/src/admesh/stl.h +++ b/src/admesh/stl.h @@ -303,6 +303,17 @@ extern bool its_write_obj(const indexed_triangle_set &its, const char *file); extern bool its_write_off(const indexed_triangle_set &its, const char *file); extern bool its_write_vrml(const indexed_triangle_set &its, const char *file); + +typedef Eigen::Matrix obj_color; // Vec3f +/// +/// write idexed triangle set into obj file with color +/// +/// input model +/// color of stored model +/// define place to store +/// True on success otherwise FALSE +extern bool its_write_obj(const indexed_triangle_set& its, const std::vector &color, const char* file); + extern bool stl_write_dxf(stl_file *stl, const char *file, char *label); inline void stl_calculate_normal(stl_normal &normal, stl_facet *facet) { normal = (facet->vertex[1] - facet->vertex[0]).cross(facet->vertex[2] - facet->vertex[0]); diff --git a/src/imgui/CMakeLists.txt b/src/imgui/CMakeLists.txt index 235afe1105..5239eb3d5c 100644 --- a/src/imgui/CMakeLists.txt +++ b/src/imgui/CMakeLists.txt @@ -3,14 +3,17 @@ project(imgui) add_library(imgui STATIC imconfig.h + imgui.cpp imgui.h + imgui_demo.cpp + imgui_draw.cpp imgui_internal.h + imgui_stdlib.cpp + imgui_stdlib.h + imgui_tables.cpp + imgui_widgets.cpp + # imgui STB imstb_rectpack.h imstb_textedit.h imstb_truetype.h - imgui_tables.cpp - imgui.cpp - imgui_demo.cpp - imgui_draw.cpp - imgui_widgets.cpp ) diff --git a/src/imgui/README.md b/src/imgui/README.md index c937d975b7..58008bded0 100644 --- a/src/imgui/README.md +++ b/src/imgui/README.md @@ -4,6 +4,19 @@ For more information go to https://github.com/ocornut/imgui THIS DIRECTORY CONTAINS THE imgui-1.83 ad5d1a8 SOURCE DISTRIBUTION. + Customized with the following commits: f93d0001baa5443da2c6510d11b03c675e652418 b71d787f695c779e571865d5214d4da8d50aa7c5 + +imgui_stdlib.h + imgui_stdlib.cpp are move from directory /imgui/misc/cpp/ +InputText() wrappers for C++ standard library (STL) type: std::string. +This is also an example of how you may wrap your own similar types. + +imstb_truetype.h modification: + +Hot fix for open symbolic fonts on windows +62bdfe6f8d04b88e8bd511cd613be80c0baa7f55 + +Hot fix for open curved fonts mainly on MAC +2148e49f75d82cb19dc6ec409fb7825296ed005c diff --git a/src/imgui/imgui_stdlib.cpp b/src/imgui/imgui_stdlib.cpp new file mode 100644 index 0000000000..cb1fe1743d --- /dev/null +++ b/src/imgui/imgui_stdlib.cpp @@ -0,0 +1,76 @@ +// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.) +// This is also an example of how you may wrap your own similar types. + +// Compatibility: +// - std::string support is only guaranteed to work from C++11. +// If you try to use it pre-C++11, please share your findings (w/ info about compiler/architecture) + +// Changelog: +// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string + +#include "imgui.h" +#include "imgui_stdlib.h" + +struct InputTextCallback_UserData +{ + std::string* Str; + ImGuiInputTextCallback ChainCallback; + void* ChainCallbackUserData; +}; + +static int InputTextCallback(ImGuiInputTextCallbackData* data) +{ + InputTextCallback_UserData* user_data = (InputTextCallback_UserData*)data->UserData; + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + // Resize string callback + // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want. + std::string* str = user_data->Str; + IM_ASSERT(data->Buf == str->c_str()); + str->resize(data->BufTextLen); + data->Buf = (char*)str->c_str(); + } + else if (user_data->ChainCallback) + { + // Forward to user callback, if any + data->UserData = user_data->ChainCallbackUserData; + return user_data->ChainCallback(data); + } + return 0; +} + +bool ImGui::InputText(const char* label, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputText(label, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, &cb_user_data); +} + +bool ImGui::InputTextMultiline(const char* label, std::string* str, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextMultiline(label, (char*)str->c_str(), str->capacity() + 1, size, flags, InputTextCallback, &cb_user_data); +} + +bool ImGui::InputTextWithHint(const char* label, const char* hint, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextWithHint(label, hint, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, &cb_user_data); +} diff --git a/src/imgui/imgui_stdlib.h b/src/imgui/imgui_stdlib.h new file mode 100644 index 0000000000..f860b0c780 --- /dev/null +++ b/src/imgui/imgui_stdlib.h @@ -0,0 +1,22 @@ +// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.) +// This is also an example of how you may wrap your own similar types. + +// Compatibility: +// - std::string support is only guaranteed to work from C++11. +// If you try to use it pre-C++11, please share your findings (w/ info about compiler/architecture) + +// Changelog: +// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string + +#pragma once + +#include + +namespace ImGui +{ + // ImGui::InputText() with std::string + // Because text input needs dynamic resizing, we need to setup a callback to grow the capacity + IMGUI_API bool InputText(const char* label, std::string* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextWithHint(const char* label, const char* hint, std::string* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); +} diff --git a/src/imgui/imstb_truetype.h b/src/imgui/imstb_truetype.h index fc815d7452..90a4a31445 100644 --- a/src/imgui/imstb_truetype.h +++ b/src/imgui/imstb_truetype.h @@ -1437,6 +1437,7 @@ static int stbtt_InitFont_internal(stbtt_fontinfo *info, unsigned char *data, in switch(ttUSHORT(data+encoding_record)) { case STBTT_PLATFORM_ID_MICROSOFT: switch (ttUSHORT(data+encoding_record+2)) { + case STBTT_MS_EID_SYMBOL: case STBTT_MS_EID_UNICODE_BMP: case STBTT_MS_EID_UNICODE_FULL: // MS/Unicode @@ -1734,7 +1735,7 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s // now start the new one start_off = !(flags & 1); - if (start_off) { + if (start_off && (i + 1) < n) { // if we start off with an off-curve point, then when we need to find a point on the curve // where we can start, and we need to save some state for when we wraparound. scx = x; diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index 8432feda74..6426b7d121 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -90,20 +90,21 @@ inline AABBTreeIndirect::Tree<2, typename LineType::Scalar> build_aabb_tree_over // Finding a closest line, its closest point and squared distance to the closest point // Returns squared distance to the closest point or -1 if the input is empty. +// or no closer point than max_sq_dist template -inline typename VectorType::Scalar squared_distance_to_indexed_lines(const std::vector &lines, - const TreeType &tree, - const VectorType &point, - size_t &hit_idx_out, - Eigen::PlainObjectBase &hit_point_out) +inline typename VectorType::Scalar squared_distance_to_indexed_lines( + const std::vector &lines, + const TreeType &tree, + const VectorType &point, + size_t &hit_idx_out, + Eigen::PlainObjectBase &hit_point_out, + typename VectorType::Scalar max_sqr_dist = std::numeric_limits::infinity()) { - using Scalar = typename VectorType::Scalar; + using Scalar = typename VectorType::Scalar; + if (tree.empty()) return Scalar(-1); auto distancer = detail::IndexedLinesDistancer{lines, tree, point}; - return tree.empty() ? - Scalar(-1) : - AABBTreeIndirect::detail::squared_distance_to_indexed_primitives_recursive(distancer, size_t(0), Scalar(0), - std::numeric_limits::infinity(), - hit_idx_out, hit_point_out); + return AABBTreeIndirect::detail::squared_distance_to_indexed_primitives_recursive( + distancer, size_t(0), Scalar(0), max_sqr_dist, hit_idx_out, hit_point_out); } // Returns all lines within the given radius limit diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index d3b7a3b856..8c879cde05 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -39,6 +39,7 @@ static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-conte const std::string AppConfig::SECTION_FILAMENTS = "filaments"; const std::string AppConfig::SECTION_MATERIALS = "sla_materials"; +const std::string AppConfig::SECTION_EMBOSS_STYLE = "font"; void AppConfig::reset() { diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index cd0f1a5aeb..5658f142d5 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -167,6 +167,7 @@ public: static const std::string SECTION_FILAMENTS; static const std::string SECTION_MATERIALS; + static const std::string SECTION_EMBOSS_STYLE; private: template diff --git a/src/libslic3r/BoundingBox.hpp b/src/libslic3r/BoundingBox.hpp index ecc36de7b2..a501680a1f 100644 --- a/src/libslic3r/BoundingBox.hpp +++ b/src/libslic3r/BoundingBox.hpp @@ -53,7 +53,7 @@ public: PointClass size() const; double radius() const; void translate(coordf_t x, coordf_t y) { assert(this->defined); PointClass v(x, y); this->min += v; this->max += v; } - void translate(const Vec2d &v) { this->min += v; this->max += v; } + void translate(const PointClass &v) { this->min += v; this->max += v; } void offset(coordf_t delta); BoundingBoxBase inflated(coordf_t delta) const throw() { BoundingBoxBase out(*this); out.offset(delta); return out; } PointClass center() const; @@ -174,6 +174,7 @@ public: BoundingBox rotated(double angle, const Point ¢er) const; void rotate(double angle) { (*this) = this->rotated(angle); } void rotate(double angle, const Point ¢er) { (*this) = this->rotated(angle, center); } + bool intersects(const BoundingBox &other) const { return this->min(0) <= other.max(0) && this->max(0) >= other.min(0) && this->min(1) <= other.max(1) && this->max(1) >= other.min(1); } // Align the min corner to a grid of cell_size x cell_size cells, // to encompass the original bounding box. void align_to_grid(const coord_t cell_size); diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 53cbd14666..bee7ceb628 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -19,6 +19,7 @@ set(SLIC3R_SOURCES pchheader.hpp AStar.hpp AABBTreeIndirect.hpp + AABBTreeLines.hpp AABBMesh.hpp AABBMesh.cpp BoundingBox.cpp @@ -42,9 +43,13 @@ set(SLIC3R_SOURCES EdgeGrid.hpp ElephantFootCompensation.cpp ElephantFootCompensation.hpp + Emboss.cpp + Emboss.hpp enum_bitmask.hpp ExPolygon.cpp ExPolygon.hpp + ExPolygonsIndex.cpp + ExPolygonsIndex.hpp Extruder.cpp Extruder.hpp ExtrusionEntity.cpp @@ -178,6 +183,7 @@ set(SLIC3R_SOURCES Model.hpp ModelArrange.hpp ModelArrange.cpp + #ModelVolumeType.hpp MultiMaterialSegmentation.cpp MultiMaterialSegmentation.hpp MeshNormals.hpp @@ -194,6 +200,8 @@ set(SLIC3R_SOURCES MutablePriorityQueue.hpp NormalUtils.cpp NormalUtils.hpp + NSVGUtils.cpp + NSVGUtils.hpp ObjectID.cpp ObjectID.hpp PerimeterGenerator.cpp @@ -266,6 +274,7 @@ set(SLIC3R_SOURCES Technologies.hpp Tesselate.cpp Tesselate.hpp + TextConfiguration.hpp TreeSupport.cpp TreeSupport.hpp TreeModelVolumes.cpp @@ -280,6 +289,8 @@ set(SLIC3R_SOURCES Utils.hpp Time.cpp Time.hpp + Timer.cpp + Timer.hpp Thread.cpp Thread.hpp TriangleSelector.cpp @@ -403,8 +414,14 @@ cmake_policy(SET CMP0011 NEW) find_package(CGAL REQUIRED) cmake_policy(POP) -add_library(libslic3r_cgal STATIC MeshBoolean.cpp MeshBoolean.hpp TryCatchSignal.hpp - TryCatchSignal.cpp Geometry/VoronoiUtilsCgal.hpp Geometry/VoronoiUtilsCgal.cpp) +add_library(libslic3r_cgal STATIC + CutSurface.hpp CutSurface.cpp + Geometry/VoronoiUtilsCgal.hpp Geometry/VoronoiUtilsCgal.cpp + IntersectionPoints.hpp IntersectionPoints.cpp + MeshBoolean.hpp MeshBoolean.cpp + TryCatchSignal.hpp TryCatchSignal.cpp + Triangulation.hpp Triangulation.cpp +) target_include_directories(libslic3r_cgal PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) # Reset compile options of libslic3r_cgal. Despite it being linked privately, CGAL options @@ -476,6 +493,7 @@ if (APPLE) # This flag prevents the need for minimum SDK version 10.14 # currently, PS targets v10.12 target_compile_options(libslic3r PUBLIC "-fno-aligned-allocation") + target_compile_options(libslic3r_cgal PUBLIC "-fno-aligned-allocation") endif () if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY) diff --git a/src/libslic3r/CutSurface.cpp b/src/libslic3r/CutSurface.cpp new file mode 100644 index 0000000000..375b6b7a0c --- /dev/null +++ b/src/libslic3r/CutSurface.cpp @@ -0,0 +1,3990 @@ +#include "CutSurface.hpp" + +/// models_input.obj - Check transormation of model to each others +/// projection_center.obj - circle representing center of projection with correct distance +/// {M} .. model index +/// model/model{M}.off - CGAL model created from index_triangle_set +/// model_neg/model{M}.off - CGAL model created for differenciate (multi volume object) +/// shape.off - CGAL model created from shapes +/// constrained/model{M}.off - Visualization of inside and outside triangles +/// Green - not along constrained edge +/// Red - sure that are inside +/// Purple - sure that are outside +/// (only along constrained edge) +/// filled/model{M}.off - flood fill green triangles inside of red area +/// - Same meaning of color as constrained +/// {N} .. Order of cutted Area of Interestmodel from model surface +/// model_AOIs/{M}/cutAOI{N}.obj - Extracted Area of interest from corefined model +/// model_AOIs/{M}/outline{N}.obj - Outline of Cutted Area +/// {O} .. Order number of patch +/// patches/patch{O}.off +/// result.obj - Merged result its +/// result_contours/{O}.obj - visualization of contours for result patches +//#define DEBUG_OUTPUT_DIR std::string("C:/data/temp/cutSurface/") + +using namespace Slic3r; +#include "ExPolygonsIndex.hpp" + +#include +#include +#include +#include +#include + +// libslic3r +#include "TriangleMesh.hpp" // its_merge +#include "Utils.hpp" // next_highest_power_of_2 +#include "ClipperUtils.hpp" // union_ex + offset_ex + +namespace priv { + +using Project = Emboss::IProjection; +using Project3d = Emboss::IProject3d; + +/// +/// Set true for indices out of area of interest +/// +/// Flag to convert triangle to cgal +/// model +/// Convert 2d point to pair of 3d points +/// 2d bounding box define AOI +void set_skip_for_out_of_aoi(std::vector &skip_indicies, + const indexed_triangle_set &its, + const Project &projection, + const BoundingBox &shapes_bb); + +/// +/// Set true for indicies outward and almost parallel together. +/// Note: internally calculate normals +/// +/// Flag to convert triangle to cgal +/// model +/// Direction to measure angle +/// Maximal allowed angle between opposit normal and +/// projection direction [in DEG] +void set_skip_by_angle(std::vector &skip_indicies, + const indexed_triangle_set &its, + const Project3d &projection, + double max_angle = 89.); + + +using EpicKernel = CGAL::Exact_predicates_inexact_constructions_kernel; +using CutMesh = CGAL::Surface_mesh; +using CutMeshes = std::vector; + +using VI = CGAL::SM_Vertex_index; +using HI = CGAL::SM_Halfedge_index; +using EI = CGAL::SM_Edge_index; +using FI = CGAL::SM_Face_index; +using P3 = CGAL::Epick::Point_3; + +/// +/// Convert triangle mesh model to CGAL Surface_mesh +/// Filtrate out opposite triangles +/// Add property map for source face index +/// +/// Model +/// Flags that triangle should be skiped +/// When true triangle will flip normal +/// CGAL mesh - half edge mesh +CutMesh to_cgal(const indexed_triangle_set &its, + const std::vector &skip_indicies, + bool flip = false); + +/// +/// Covert 2d shape (e.g. Glyph) to CGAL model +/// NOTE: internaly create +/// edge_shape_map .. Property map to store conversion from edge to contour +/// face_shape_map .. Property map to store conversion from face to contour +/// +/// 2d shapes to project +/// Define transformation 2d point into 3d +/// CGAL model of extruded shape +CutMesh to_cgal(const ExPolygons &shapes, const Project &projection); +// function to check result of projection. 2d int32_t -> 3d double +bool exist_duplicit_vertex(const CutMesh& mesh); + + +/// +/// IntersectingElement +/// +/// Adress polygon inside of ExPolygon +/// Keep information about source of vertex: +/// - from face (one of 2 possible) +/// - from edge (one of 2 possible) +/// +/// V1~~~~~V2 +/// | f1 /: +/// | / : +/// e1| /e2: +/// | / : +/// |/ f2 : +/// V1'~~~~V2' +/// +/// | .. edge +/// / .. edge +/// : .. foreign edge - neighbor +/// ~ .. no care edge - idealy should not cross model +/// V1,V1' .. projected 2d point to 3d +/// V2,V2' .. projected 2d point to 3d +/// +/// Vertex indexing +/// V1 .. i (vertex_base + 2x index of point in polygon) +/// V1' .. i + 1 +/// V2 .. j = i + 2 || 0 (for last i in polygon) +/// V2' .. j + 1 +/// +/// f1 .. text_face_1 (triangle face made by side of shape contour) +/// f2 .. text_face_2 +/// e1 .. text_edge_1 (edge on side of face made by side of shape contour) +/// e2 .. text_edge_2 +/// +/// +struct IntersectingElement +{ + // identify source point in shapes + uint32_t shape_point_index{std::numeric_limits::max()}; + + // store together type, is_first, is_last + unsigned char attr{std::numeric_limits::max()}; + + // vertex or edge ID, where edge ID is the index of the source point. + // There are 4 consecutive indices generated for a single contour edge: + // 0th - 1st text edge (straight) + // 1th - 1st text face + // 2nd - 2nd text edge (diagonal) + // 3th - 2nd text face + // Type of intersecting element from extruded shape( 3d ) + // NOTE: type must be storable to 3bit -> max value is 7 + enum class Type: unsigned char { + edge_1 = 0, + face_1 = 1, + edge_2 = 2, + face_2 = 3, + undefined = 4 + }; + + IntersectingElement &set_type(Type t) + { + attr = static_cast( + attr + (int) t - (int) get_type()); + return *this; + } + void set_is_first(){ attr += 8; } + void set_is_last(){ attr += 16; } + Type get_type() const { return static_cast(attr % 8);} + bool is_first() const { return 8 <= attr && attr < 16; } + bool is_last() const { return attr >= 16; } +}; + +// stored in model made by shape +using EdgeShapeMap = CutMesh::Property_map; +using FaceShapeMap = CutMesh::Property_map; + +// stored in surface source - pointer to EdgeShapeMap | FaceShapeMap +using VertexShapeMap = CutMesh::Property_map; + +// stored in model made by shape +const std::string edge_shape_map_name = "e:IntersectingElement"; +const std::string face_shape_map_name = "f:IntersectingElement"; + +// stored in surface source +const std::string vert_shape_map_name = "v:IntersectingElement"; + +/// +/// Flag for faces in CGAL mesh +/// +enum class FaceType { + // face inside of the cutted shape + inside, + // face outside of the cutted shape + outside, + // face without constrained edge (In or Out) + not_constrained, + + // Helper flag that inside was processed + inside_processed +}; +using FaceTypeMap = CutMesh::Property_map; +const std::string face_type_map_name = "f:side"; + +// Conversion one vertex index to another +using CvtVI2VI = CutMesh::Property_map; +// Each Patch track outline vertex conversion to tource model +const std::string patch_source_name = "v:patch_source"; + +// For VI that should be reduced, contain VI to use instead of reduced +// Other VI are invalid +using ReductionMap = CvtVI2VI; +const std::string vertex_reduction_map_name = "v:reduction"; + +// A property map containing the constrained-or-not status of each edge +using EdgeBoolMap = CutMesh::Property_map; +const std::string is_constrained_edge_name = "e:is_constrained"; + +/// +/// Create map to reduce unnecesary triangles, +/// Triangles are made by divided quad to two triangles +/// on side of cutting shape mesh +/// Note: also use from mesh (have to be created) +/// face_type_map .. Type of shape inside / outside +/// vert_shape_map .. Source of outline vertex +/// +/// Reduction map from vertex to vertex, +/// when key == value than no reduction +/// Faces of one +/// Input object +void create_reduce_map(ReductionMap &reduction_map, const CutMesh &meshes); + +// Patch made by Cut area of interest from model +// connected faces(triangles) and outlines(halfEdges) for one surface cut +using CutAOI = std::pair, std::vector>; +// vector of Cutted Area of interest cutted from one CGAL model +using CutAOIs = std::vector; +// vector of CutAOIs for each model +using VCutAOIs = std::vector; + +/// +/// Create AOIs(area of interest) on model surface +/// +/// Input model converted to CGAL +/// NOTE: will be extended by corefine edge +/// 2d contours +/// [const]Model made by shapes +/// NOTE: Can't be definde as const because of corefine function input definition, +/// but it is. +/// Wanted projection distance +/// Convert index to shape point from ExPolygons +/// Patches from model surface +CutAOIs cut_from_model(CutMesh &cgal_model, + const ExPolygons &shapes, + /*const*/ CutMesh &cgal_shape, + float projection_ratio, + const ExPolygonsIndices &s2i); + +using Loop = std::vector; +using Loops = std::vector; + +/// +/// Create closed loops of contour vertices created from open half edges +/// +/// Unsorted half edges +/// Source mesh for half edges +/// Closed loops +Loops create_loops(const std::vector &outlines, const CutMesh &mesh); + +// To track during diff_models, +// what was cutted off, from CutAOI +struct SurfacePatch +{ + // converted cut to CGAL mesh + // Mesh is reduced. + // (do not contain divided triangles on contour - created by side Quad) + CutMesh mesh; + // CvtVI2VI cvt = mesh.property_map(patch_source_name); + // Conversion VI from this patch to source VI(model) is stored in mesh property + + // Outlines - converted CutAOI.second (half edges) + // to loops (vertex indicies) by function create_loops + Loops loops; + + // bounding box of mesh + BoundingBoxf3 bb; + + //// Data needed to find best projection distances + // index of source model in models + size_t model_id; + // index of source CutAOI + size_t aoi_id; + // index of shape from ExPolygons + size_t shape_id = 0; + + // flag that this patch contain whole CutAOI + bool is_whole_aoi = true; +}; +using SurfacePatches = std::vector; + +struct ModelCutId +{ + // index of model + uint32_t model_index; + // index of cut inside model + uint32_t cut_index; +}; + +/// +/// Keep conversion from VCutAOIs to Index and vice versa +/// Model_index .. contour(or hole) poin from ExPolygons +/// Index .. continous number +/// +class ModelCut2index +{ + std::vector m_offsets; + // for check range of index + uint32_t m_count; + +public: + ModelCut2index(const VCutAOIs &cuts); + uint32_t calc_index(const ModelCutId &id) const; + ModelCutId calc_id(uint32_t index) const; + uint32_t get_count() const { return m_count; }; + const std::vector &get_offsets() const { return m_offsets; } +}; + +/// +/// Differenciate other models +/// +/// Patches from meshes +/// Source points for Cutted AOIs +/// NOTE: Create Reduction map as mesh property - clean on end +/// Original models without cut modifications +/// used for differenciation +/// NOTE: Clip function modify Mesh +/// Define projection direction +/// Cuts differenciate by models - Patch +SurfacePatches diff_models(VCutAOIs &cuts, + /*const*/ CutMeshes &cut_models, + /*const*/ CutMeshes &models, + const Project3d &projection); + +/// +/// Checking whether patch is uninterrupted cover of whole expolygon it belongs. +/// +/// Part of surface to check +/// Source shape +/// Source of cut +/// True when cover whole expolygon otherwise false +bool is_over_whole_expoly(const CutAOI &cutAOI, + const ExPolygon &shape, + const CutMesh &mesh); + +/// +/// Checking whether patch is uninterrupted cover of whole expolygon it belongs. +/// +/// Part of surface to check +/// Source shape +/// True when cover whole expolygon otherwise false +bool is_over_whole_expoly(const SurfacePatch &patch, + const ExPolygons &shapes, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes); +/// +/// Unptoject points from outline loops of patch +/// +/// Contain loops and vertices +/// Know how to project from 3d to 2d +/// Range of unprojected points x .. min, y .. max value +/// Unprojected points in loops +Polygons unproject_loops(const SurfacePatch &patch, const Project &projection, Vec2d &depth_range); + +/// +/// Unproject points from loops and create expolygons +/// +/// Patch to convert on expolygon +/// Convert 3d point to 2d +/// Range of unprojected points x .. min, y .. max value +/// Expolygon represent patch in 2d +ExPolygon to_expoly(const SurfacePatch &patch, const Project &projection, Vec2d &depth_range); + +/// +/// To select surface near projection distance +/// +struct ProjectionDistance +{ + // index of source model + uint32_t model_index = std::numeric_limits::max(); + + // index of CutAOI + uint32_t aoi_index = std::numeric_limits::max(); + + // index of Patch + uint32_t patch_index = std::numeric_limits::max(); + + // signed distance to projection + float distance = std::numeric_limits::max(); +}; +// addresed by ExPolygonsIndices +using ProjectionDistances = std::vector; + +// each point in shapes has its ProjectionDistances +using VDistances = std::vector; + +/// +/// Calculate distances for SurfacePatches outline points +/// NOTE: +/// each model has to have "vert_shape_map" .. Know source of new vertices +/// +/// Part of surface +/// Vertices position +/// Mesh created by shapes +/// Count of contour points in shapes +/// Define best distnace +/// Projection distances of cutted shape points +VDistances calc_distances(const SurfacePatches &patches, + const CutMeshes &models, + const CutMesh &shapes_mesh, + size_t count_shapes_points, + float projection_ratio); + +/// +/// Select distances in similar depth between expolygons +/// +/// All distances - Vector distances for each shape point +/// Vector of letters +/// Pivot for start projection in 2d +/// Convert index to addresss inside of shape +/// Cutted parts from surface +/// Closest distance projection indexed by points in shapes(see s2i) +ProjectionDistances choose_best_distance( + const VDistances &distances, + const ExPolygons &shapes, + const Point &start, + const ExPolygonsIndices &s2i, + const SurfacePatches &patches); + +/// +/// Create mask for patches +/// +/// For each point selected closest distance +/// All patches +/// All patches +/// Mask of used patch +std::vector select_patches(const ProjectionDistances &best_distances, + const SurfacePatches &patches, + + const ExPolygons &shapes, + const ExPolygonsIndices &s2i, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes, + const Project &projection); + +/// +/// Merge two surface cuts together +/// Added surface cut will be consumed +/// +/// Surface cut to extend +/// Surface cut to consume +void append(SurfaceCut &sc, SurfaceCut &&sc_add); + +/// +/// Convert patch to indexed_triangle_set +/// +/// Part of surface +/// Converted patch +SurfaceCut patch2cut(SurfacePatch &patch); + +/// +/// Merge masked patches to one surface cut +/// +/// All patches +/// NOTE: Not const because One needs to add property for Convert indices +/// Mash for using patch +/// Result surface cut +SurfaceCut merge_patches(/*const*/ SurfacePatches &patches, + const std::vector &mask); + +#ifdef DEBUG_OUTPUT_DIR +void prepare_dir(const std::string &dir); +void initialize_store(const std::string &dir_to_clear); +/// +/// Debug purpose store of mesh with colored face by face type +/// +/// Input mesh, could add property color +/// NOTE: Not const because need to [optionaly] append color property map +/// Color source +/// File to store +void store(const CutMesh &mesh, const FaceTypeMap &face_type_map, const std::string &dir, bool is_filled = false); +void store(const ExPolygons &shapes, const std::string &svg_file); +void store(const CutMesh &mesh, const ReductionMap &reduction_map, const std::string &dir); +void store(const CutAOIs &aois, const CutMesh &mesh, const std::string &dir); +void store(const SurfacePatches &patches, const std::string &dir); +void store(const Vec3f &vertex, const Vec3f &normal, const std::string &file, float size = 2.f); +//void store(const ProjectionDistances &pds, const VCutAOIs &aois, const CutMeshes &meshes, const std::string &file, float width = 0.2f/* [in mm] */); +using Connection = std::pair; using Connections = std::vector; +void store(const ExPolygons &shapes, const std::vector &mask_distances, const Connections &connections, const std::string &file_svg); +void store(const SurfaceCut &cut, const std::string &file, const std::string &contour_dir); +void store(const std::vector &models, const std::string &obj_filename); +void store(const std::vector&models, const std::string &dir); +void store(const Emboss::IProjection &projection, const Point &point_to_project, float projection_ratio, const std::string &obj_filename); +#endif // DEBUG_OUTPUT_DIR +} // namespace privat + +#ifdef DEBUG_OUTPUT_DIR +#include "libslic3r/SVG.hpp" +#include +#include +#endif // DEBUG_OUTPUT_DIR + +SurfaceCut Slic3r::cut_surface(const ExPolygons &shapes, + const std::vector &models, + const Emboss::IProjection &projection, + float projection_ratio) +{ + assert(!models.empty()); + assert(!shapes.empty()); + if (models.empty() || shapes.empty() ) return {}; + +#ifdef DEBUG_OUTPUT_DIR + priv::initialize_store(DEBUG_OUTPUT_DIR); + priv::store(models, DEBUG_OUTPUT_DIR + "models_input.obj"); + priv::store(shapes, DEBUG_OUTPUT_DIR + "shapes.svg"); +#endif // DEBUG_OUTPUT_DIR + + // for filter out triangles out of bounding box + BoundingBox shapes_bb = get_extents(shapes); +#ifdef DEBUG_OUTPUT_DIR + priv::store(projection, shapes_bb.center(), projection_ratio, DEBUG_OUTPUT_DIR + "projection_center.obj"); +#endif // DEBUG_OUTPUT_DIR + + // for filttrate opposite triangles and a little more + const float max_angle = 89.9f; + priv::CutMeshes cgal_models; // source for patch + priv::CutMeshes cgal_neg_models; // model used for differenciate patches + cgal_models.reserve(models.size()); + for (const indexed_triangle_set &its : models) { + std::vector skip_indicies(its.indices.size(), {false}); + priv::set_skip_for_out_of_aoi(skip_indicies, its, projection, shapes_bb); + + // create model for differenciate cutted patches + bool flip = true; + cgal_neg_models.push_back(priv::to_cgal(its, skip_indicies, flip)); + + // cut out more than only opposit triangles + priv::set_skip_by_angle(skip_indicies, its, projection, max_angle); + cgal_models.push_back(priv::to_cgal(its, skip_indicies)); + } +#ifdef DEBUG_OUTPUT_DIR + priv::store(cgal_models, DEBUG_OUTPUT_DIR + "model/");// model[0-N].off + priv::store(cgal_neg_models, DEBUG_OUTPUT_DIR + "model_neg/"); // model[0-N].off +#endif // DEBUG_OUTPUT_DIR + + priv::CutMesh cgal_shape = priv::to_cgal(shapes, projection); +#ifdef DEBUG_OUTPUT_DIR + CGAL::IO::write_OFF(DEBUG_OUTPUT_DIR + "shape.off", cgal_shape); // only debug +#endif // DEBUG_OUTPUT_DIR + + // create tool for convert index to shape Point adress and vice versa + ExPolygonsIndices s2i(shapes); + priv::VCutAOIs model_cuts; + // cut shape from each cgal model + for (priv::CutMesh &cgal_model : cgal_models) { + priv::CutAOIs cutAOIs = priv::cut_from_model( + cgal_model, shapes, cgal_shape, projection_ratio, s2i); +#ifdef DEBUG_OUTPUT_DIR + size_t index = &cgal_model - &cgal_models.front(); + priv::store(cutAOIs, cgal_model, DEBUG_OUTPUT_DIR + "model_AOIs/" + std::to_string(index) + "/"); // only debug +#endif // DEBUG_OUTPUT_DIR + model_cuts.push_back(std::move(cutAOIs)); + } + + priv::SurfacePatches patches = priv::diff_models(model_cuts, cgal_models, cgal_neg_models, projection); +#ifdef DEBUG_OUTPUT_DIR + priv::store(patches, DEBUG_OUTPUT_DIR + "patches/"); +#endif // DEBUG_OUTPUT_DIR + if (patches.empty()) return {}; + + // fix - convert shape_point_id to expolygon index + // save 1 param(s2i) from diff_models call + for (priv::SurfacePatch &patch : patches) + patch.shape_id = s2i.cvt(patch.shape_id).expolygons_index; + + // calc distance to projection for all outline points of cutAOI(shape) + // it is used for distiguish the top one + uint32_t shapes_points = s2i.get_count(); + // for each point collect all projection distances + priv::VDistances distances = priv::calc_distances(patches, cgal_models, cgal_shape, shapes_points, projection_ratio); + + Point start = shapes_bb.center(); // only align center + + // Use only outline points + // for each point select best projection + priv::ProjectionDistances best_projection = priv::choose_best_distance(distances, shapes, start, s2i, patches); + std::vector use_patch = priv::select_patches(best_projection, patches, + shapes, s2i,model_cuts, cgal_models, projection); + SurfaceCut result = merge_patches(patches, use_patch); + //*/ + +#ifdef DEBUG_OUTPUT_DIR + priv::store(result, DEBUG_OUTPUT_DIR + "result.obj", DEBUG_OUTPUT_DIR + "result_contours/"); +#endif // DEBUG_OUTPUT_DIR + return result; +} + +indexed_triangle_set Slic3r::cut2model(const SurfaceCut &cut, + const Emboss::IProject3d &projection) +{ + assert(!cut.empty()); + size_t count_vertices = cut.vertices.size() * 2; + size_t count_indices = cut.indices.size() * 2; + + // indices from from zig zag + for (const auto &c : cut.contours) { + assert(!c.empty()); + count_indices += c.size() * 2; + } + + indexed_triangle_set result; + result.vertices.reserve(count_vertices); + result.indices.reserve(count_indices); + + // front + result.vertices.insert(result.vertices.end(), + cut.vertices.begin(), cut.vertices.end()); + result.indices.insert(result.indices.end(), + cut.indices.begin(), cut.indices.end()); + + // back + for (const Vec3f &v : cut.vertices) { + Vec3d vd = v.cast(); + Vec3d vd2 = projection.project(vd); + result.vertices.push_back(vd2.cast()); + } + + size_t back_offset = cut.vertices.size(); + for (const auto &i : cut.indices) { + // check range of indices in cut + assert(i.x() + back_offset < result.vertices.size()); + assert(i.y() + back_offset < result.vertices.size()); + assert(i.z() + back_offset < result.vertices.size()); + assert(i.x() >= 0 && i.x() < cut.vertices.size()); + assert(i.y() >= 0 && i.y() < cut.vertices.size()); + assert(i.z() >= 0 && i.z() < cut.vertices.size()); + // Y and Z is swapped CCW triangles for back side + result.indices.emplace_back(i.x() + back_offset, + i.z() + back_offset, + i.y() + back_offset); + } + + // zig zag indices + for (const auto &contour : cut.contours) { + size_t prev_front_index = contour.back(); + size_t prev_back_index = back_offset + prev_front_index; + for (size_t front_index : contour) { + assert(front_index < cut.vertices.size()); + size_t back_index = back_offset + front_index; + result.indices.emplace_back(front_index, prev_front_index, back_index); + result.indices.emplace_back(prev_front_index, prev_back_index, back_index); + prev_front_index = front_index; + prev_back_index = back_index; + } + } + + assert(count_vertices == result.vertices.size()); + assert(count_indices == result.indices.size()); + return result; +} + +// set_skip_for_out_of_aoi helping functions +namespace priv { +// define plane +using PointNormal = std::pair; +using PointNormals = std::array; + +/// +/// Check +/// +/// +/// +/// +/// +bool is_out_of(const Vec3d &v, const PointNormal &point_normal); + +using IsOnSides = std::vector>; +/// +/// Check if triangle t has all vertices out of any plane +/// +/// Triangle +/// Flag is vertex index out of plane +/// True when triangle is out of one of plane +bool is_all_on_one_side(const Vec3i &t, const IsOnSides& is_on_sides); + +} // namespace priv + +bool priv::is_out_of(const Vec3d &v, const PointNormal &point_normal) +{ + const Vec3d& p = point_normal.first; + const Vec3d& n = point_normal.second; + double signed_distance = (v - p).dot(n); + return signed_distance > 1e-5; +}; + +bool priv::is_all_on_one_side(const Vec3i &t, const IsOnSides& is_on_sides) { + for (size_t side = 0; side < 4; side++) { + bool result = true; + for (auto vi : t) { + if (!is_on_sides[vi][side]) { + result = false; + break; + } + } + if (result) return true; + } + return false; +} + +void priv::set_skip_for_out_of_aoi(std::vector &skip_indicies, + const indexed_triangle_set &its, + const Project &projection, + const BoundingBox &shapes_bb) +{ + assert(skip_indicies.size() == its.indices.size()); + // 1`*----* 2` + // / 2 /| + // 1 *----* | + // | | * 3` + // | |/ + // 0 *----* 3 + ////////////////// + std::array, 4> bb; + int index = 0; + for (Point v : + {shapes_bb.min, Point{shapes_bb.min.x(), shapes_bb.max.y()}, + shapes_bb.max, Point{shapes_bb.max.x(), shapes_bb.min.y()}}) + bb[index++] = projection.create_front_back(v); + + // define planes to test + // 0 .. under + // 1 .. left + // 2 .. above + // 3 .. right + size_t prev_i = 3; + // plane is defined by point and normal + PointNormals point_normals; + for (size_t i = 0; i < 4; i++) { + const Vec3d &p1 = bb[i].first; + const Vec3d &p2 = bb[i].second; + const Vec3d &p3 = bb[prev_i].first; + prev_i = i; + + Vec3d v1 = p2 - p1; + v1.normalize(); + Vec3d v2 = p3 - p1; + v2.normalize(); + + Vec3d normal = v2.cross(v1); + normal.normalize(); + + point_normals[i] = {p1, normal}; + } + // same meaning as point normal + IsOnSides is_on_sides(its.vertices.size(), {false,false,false,false}); + + // inspect all vertices when it is out of bounding box + tbb::parallel_for(tbb::blocked_range(0, its.vertices.size()), + [&its, &point_normals, &is_on_sides](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + Vec3d v = its.vertices[i].cast(); + // under + above + for (int side : {0, 2}) { + if (is_out_of(v, point_normals[side])) { + is_on_sides[i][side] = true; + // when it is under it can't be above + break; + } + } + // left + right + for (int side : {1, 3}) { + if (is_out_of(v, point_normals[side])) { + is_on_sides[i][side] = true; + // when it is on left side it can't be on right + break; + } + } + } + }); // END parallel for + + // inspect all triangles, when it is out of bounding box + tbb::parallel_for(tbb::blocked_range(0, its.indices.size()), + [&its, &is_on_sides, &skip_indicies](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + if (is_all_on_one_side(its.indices[i], is_on_sides)) + skip_indicies[i] = true; + } + }); // END parallel for +} + +indexed_triangle_set Slic3r::its_mask(const indexed_triangle_set &its, + const std::vector &mask) +{ + if (its.indices.size() != mask.size()) { + assert(false); + return {}; + } + + std::vector cvt_vetices(its.vertices.size(), {std::numeric_limits::max()}); + size_t vertices_count = 0; + size_t faces_count = 0; + for (const auto &t : its.indices) { + size_t index = &t - &its.indices.front(); + if (!mask[index]) continue; + ++faces_count; + for (const auto vi : t) { + uint32_t &cvt = cvt_vetices[vi]; + if (cvt == std::numeric_limits::max()) + cvt = vertices_count++; + } + } + if (faces_count == 0) return {}; + + indexed_triangle_set result; + result.indices.reserve(faces_count); + result.vertices = std::vector(vertices_count); + for (size_t i = 0; i < its.vertices.size(); ++i) { + uint32_t index = cvt_vetices[i]; + if (index == std::numeric_limits::max()) continue; + result.vertices[index] = its.vertices[i]; + } + + for (const stl_triangle_vertex_indices &f : its.indices) + if (mask[&f - &its.indices.front()]) + result.indices.push_back(stl_triangle_vertex_indices( + cvt_vetices[f[0]], cvt_vetices[f[1]], cvt_vetices[f[2]])); + + return result; +} + +indexed_triangle_set Slic3r::its_cut_AoI(const indexed_triangle_set &its, + const BoundingBox &bb, + const Emboss::IProjection &projection) +{ + std::vector skip_indicies(its.indices.size(), false); + priv::set_skip_for_out_of_aoi(skip_indicies, its, projection, bb); + // invert values in vector of bool + skip_indicies.flip(); + return its_mask(its, skip_indicies); +} + +void priv::set_skip_by_angle(std::vector &skip_indicies, + const indexed_triangle_set &its, + const Project3d &projection, + double max_angle) +{ + assert(max_angle < 90. && max_angle > 89.); + assert(skip_indicies.size() == its.indices.size()); + float threshold = static_cast(cos(max_angle / 180. * M_PI)); + for (const stl_triangle_vertex_indices& face : its.indices) { + size_t index = &face - &its.indices.front(); + if (skip_indicies[index]) continue; + Vec3f n = its_face_normal(its, face); + const Vec3f& v = its.vertices[face[0]]; + const Vec3d vd = v.cast(); + // Improve: For Orthogonal Projection it is same for each vertex + Vec3d projectedd = projection.project(vd); + Vec3f projected = projectedd.cast(); + Vec3f project_dir = projected - v; + project_dir.normalize(); + float cos_alpha = project_dir.dot(n); + if (cos_alpha > threshold) continue; + skip_indicies[index] = true; + } +} + +priv::CutMesh priv::to_cgal(const indexed_triangle_set &its, + const std::vector &skip_indicies, + bool flip) +{ + const std::vector &vertices = its.vertices; + const std::vector &indices = its.indices; + + std::vector use_vetices(vertices.size(), {false}); + + size_t vertices_count = 0; + size_t faces_count = 0; + size_t edges_count = 0; + + for (const auto &t : indices) { + size_t index = &t - &indices.front(); + if (skip_indicies[index]) continue; + ++faces_count; + size_t count_used_vertices = 0; + for (const auto vi : t) { + if (!use_vetices[vi]) { + ++vertices_count; + use_vetices[vi] = true; + } else { + ++count_used_vertices; + } + } + switch (count_used_vertices) { + case 3: break; // all edges are already counted + case 2: edges_count += 2; break; + case 1: + case 0: edges_count += 3; break; + default: assert(false); + } + } + assert(vertices_count <= vertices.size()); + assert(edges_count <= (indices.size() * 3)); + assert(faces_count <= indices.size()); + + CutMesh result; + result.reserve(vertices_count, edges_count, faces_count); + + std::vector to_filtrated_vertices_index(vertices.size()); + size_t filtrated_vertices_index = 0; + for (size_t i = 0; i < vertices.size(); ++i) + if (use_vetices[i]) { + to_filtrated_vertices_index[i] = VI(filtrated_vertices_index); + ++filtrated_vertices_index; + } + + for (const stl_vertex& v : vertices) { + if (!use_vetices[&v - &vertices.front()]) continue; + result.add_vertex(CutMesh::Point{v.x(), v.y(), v.z()}); + } + + if (!flip) { + for (const stl_triangle_vertex_indices &f : indices) { + if (skip_indicies[&f - &indices.front()]) continue; + result.add_face(to_filtrated_vertices_index[f[0]], + to_filtrated_vertices_index[f[1]], + to_filtrated_vertices_index[f[2]]); + } + } else { + for (const stl_triangle_vertex_indices &f : indices) { + if (skip_indicies[&f - &indices.front()]) continue; + result.add_face(to_filtrated_vertices_index[f[2]], + to_filtrated_vertices_index[f[1]], + to_filtrated_vertices_index[f[0]]); + } + } + + return result; +} + +bool priv::exist_duplicit_vertex(const CutMesh &mesh) { + std::vector points; + points.reserve(mesh.vertices().size()); + // copy points + for (VI vi : mesh.vertices()) { + const P3 &p = mesh.point(vi); + points.emplace_back(p.x(), p.y(), p.z()); + } + std::sort(points.begin(), points.end(), [](const Vec3d &v1, const Vec3d &v2) { + return v1.x() < v2.x() || + (v1.x() == v2.x() && + (v1.y() < v2.y() || + (v1.y() == v2.y() && + v1.z() < v2.z()))); + }); + // find first duplicit + auto it = std::adjacent_find(points.begin(), points.end()); + return it != points.end(); +} + +priv::CutMesh priv::to_cgal(const ExPolygons &shapes, + const Project &projection) +{ + if (shapes.empty()) return {}; + + CutMesh result; + EdgeShapeMap edge_shape_map = result.add_property_map(edge_shape_map_name).first; + FaceShapeMap face_shape_map = result.add_property_map(face_shape_map_name).first; + + std::vector indices; + auto insert_contour = [&projection, &indices, &result, + &edge_shape_map, &face_shape_map] + (const Polygon &polygon) { + indices.clear(); + indices.reserve(polygon.points.size() * 2); + size_t num_vertices_old = result.number_of_vertices(); + for (const Point &polygon_point : polygon.points) { + auto [front, back] = projection.create_front_back(polygon_point); + P3 v_front{front.x(), front.y(), front.z()}; + VI vi1 = result.add_vertex(v_front); + assert(vi1.idx() == (indices.size() + num_vertices_old)); + indices.push_back(vi1); + + P3 v_back{back.x(), back.y(), back.z()}; + VI vi2 = result.add_vertex(v_back); + assert(vi2.idx() == (indices.size() + num_vertices_old)); + indices.push_back(vi2); + } + + auto find_edge = [&result](FI fi, VI from, VI to) { + HI hi = result.halfedge(fi); + for (; result.target(hi) != to; hi = result.next(hi)); + assert(result.source(hi) == from); + assert(result.target(hi) == to); + return result.edge(hi); + }; + + uint32_t contour_index = static_cast(num_vertices_old / 2); + for (int32_t i = 0; i < int32_t(indices.size()); i += 2) { + bool is_first = i == 0; + bool is_last = size_t(i + 2) >= indices.size(); + int32_t j = is_last ? 0 : (i + 2); + + FI fi1 = result.add_face(indices[i], indices[j], indices[i + 1]); + EI ei1 = find_edge(fi1, indices[i + 1], indices[i]); + EI ei2 = find_edge(fi1, indices[j], indices[i + 1]); + FI fi2 = result.add_face(indices[j], indices[j + 1], indices[i + 1]); + IntersectingElement element {contour_index, (unsigned char)IntersectingElement::Type::undefined}; + if (is_first) element.set_is_first(); + if (is_last) element.set_is_last(); + edge_shape_map[ei1] = element.set_type(IntersectingElement::Type::edge_1); + face_shape_map[fi1] = element.set_type(IntersectingElement::Type::face_1); + edge_shape_map[ei2] = element.set_type(IntersectingElement::Type::edge_2); + face_shape_map[fi2] = element.set_type(IntersectingElement::Type::face_2); + ++contour_index; + } + }; + + size_t count_point = count_points(shapes); + result.reserve(result.number_of_vertices() + 2 * count_point, + result.number_of_edges() + 4 * count_point, + result.number_of_faces() + 2 * count_point); + + // Identify polygon + for (const ExPolygon &shape : shapes) { + insert_contour(shape.contour); + for (const Polygon &hole : shape.holes) + insert_contour(hole); + } + assert(!exist_duplicit_vertex(result)); + return result; +} + +priv::ModelCut2index::ModelCut2index(const VCutAOIs &cuts) +{ + // prepare offsets + m_offsets.reserve(cuts.size()); + uint32_t offset = 0; + for (const CutAOIs &model_cuts: cuts) { + m_offsets.push_back(offset); + offset += model_cuts.size(); + } + m_count = offset; +} + +uint32_t priv::ModelCut2index::calc_index(const ModelCutId &id) const +{ + assert(id.model_index < m_offsets.size()); + uint32_t offset = m_offsets[id.model_index]; + uint32_t res = offset + id.cut_index; + assert(((id.model_index+1) < m_offsets.size() && res < m_offsets[id.model_index+1]) || + ((id.model_index+1) == m_offsets.size() && res < m_count)); + return res; +} + +priv::ModelCutId priv::ModelCut2index::calc_id(uint32_t index) const +{ + assert(index < m_count); + ModelCutId result{0,0}; + // find shape index + for (size_t model_index = 1; model_index < m_offsets.size(); ++model_index) { + if (m_offsets[model_index] > index) break; + result.model_index = model_index; + } + result.cut_index = index - m_offsets[result.model_index]; + return result; +} + +// cut_from_model help functions +namespace priv { + +/// +/// Track source of intersection +/// Help for anotate inner and outer faces +/// +struct Visitor { + const CutMesh &object; + const CutMesh &shape; + + // Properties of the shape mesh: + EdgeShapeMap edge_shape_map; + FaceShapeMap face_shape_map; + + // Properties of the object mesh. + VertexShapeMap vert_shape_map; + + // check for anomalities + bool* is_valid; + + // keep source of intersection for each intersection + // used to copy data into vert_shape_map + std::vector intersections; + + /// + /// Called when a new intersection point is detected. + /// The intersection is detected using a face of tm_f and an edge of tm_e. + /// Intersecting an edge hh_edge from tm_f with a face h_e of tm_e. + /// https://doc.cgal.org/latest/Polygon_mesh_processing/classPMPCorefinementVisitor.html#a00ee0ca85db535a48726a92414acda7f + /// + /// The id of the intersection point, starting at 0. Ids are consecutive. + /// Dimension of a simplex part of face(h_e) that is intersected by edge(h_f): + /// 0 for vertex: target(h_e) + /// 1 for edge: h_e + /// 2 for the interior of face: face(h_e) + /// + /// A halfedge from tm_f indicating the simplex intersected: + /// if sdim==0 the target of h_f is the intersection point, + /// if sdim==1 the edge of h_f contains the intersection point in its interior, + /// if sdim==2 the face of h_f contains the intersection point in its interior. + /// @Vojta: Edge of tm_f, see is_target_coplanar & is_source_coplanar whether any vertex of h_f is coplanar with face(h_e). + /// + /// A halfedge from tm_e + /// @Vojta: Vertex, halfedge or face of tm_e intersected by h_f, see comment at sdim. + /// + /// Mesh containing h_f + /// Mesh containing h_e + /// True if the target of h_e is the intersection point + /// @Vojta: source(h_f) is coplanar with face(made by h_e). + /// True if the source of h_e is the intersection point + /// @Vojta: target(h_f) is coplanar with face(h_e). + void intersection_point_detected(std::size_t i_id, + int sdim, + HI h_f, + HI h_e, + const CutMesh &tm_f, + const CutMesh &tm_e, + bool is_target_coplanar, + bool is_source_coplanar); + + /// + /// Called when a new vertex is added in tm (either an edge split or a vertex inserted in the interior of a face). + /// Fill vertex_shape_map by intersections + /// + /// Order number of intersection point + /// New added vertex + /// Affected mesh + void new_vertex_added(std::size_t i_id, VI v, const CutMesh &tm); + + // Not used visitor functions + void before_subface_creations(FI /* f_old */, CutMesh &/* mesh */){} + void after_subface_created(FI /* f_new */, CutMesh &/* mesh */) {} + void after_subface_creations(CutMesh&) {} + void before_subface_created(CutMesh&) {} + void before_edge_split(HI /* h */, CutMesh& /* tm */) {} + void edge_split(HI /* hnew */, CutMesh& /* tm */) {} + void after_edge_split() {} + void add_retriangulation_edge(HI /* h */, CutMesh& /* tm */) {} +}; + +/// +/// Distiquish face type for half edge +/// +/// Define face +/// Mesh to process +/// Vertices of mesh made by shapes +/// Keep information about source of created vertex +/// +/// Convert index to shape point from ExPolygons +/// Face type defined by hi +bool is_face_inside(HI hi, + const CutMesh &mesh, + const CutMesh &shape_mesh, + const VertexShapeMap &vertex_shape_map, + const ExPolygonsIndices &shape2index); + +/// +/// Face with constrained edge are inside/outside by type of intersection +/// Other set to not_constrained(still it could be inside/outside) +/// +/// [Output] property map with type of faces +/// Mesh to process +/// Keep information about source of created vertex +/// Dynamic Edge Constrained Map of bool +/// Vertices of mesh made by shapes +/// Convert index to shape point from ExPolygons +void set_face_type(FaceTypeMap &face_type_map, + const CutMesh &mesh, + const VertexShapeMap &vertex_shape_map, + const EdgeBoolMap &ecm, + const CutMesh &shape_mesh, + const ExPolygonsIndices &shape2index); + +/// +/// Change FaceType from not_constrained to inside +/// For neighbor(or neighbor of neighbor of ...) of inside triangles. +/// Process only not_constrained triangles +/// +/// Corefined mesh +/// In/Out map with faces type +void flood_fill_inner(const CutMesh &mesh, FaceTypeMap &face_type_map); + +/// +/// Collect connected inside faces +/// Collect outline half edges +/// +/// Queue of face to process - find connected +/// [Output] collected Face indices from mesh +/// [Output] collected Halfedge indices from mesh +/// Use flag inside / outside +/// NOTE: Modify in function: inside -> inside_processed +/// mesh to process +void collect_surface_data(std::queue &process, + std::vector &faces, + std::vector &outlines, + FaceTypeMap &face_type_map, + const CutMesh &mesh); + +/// +/// Create areas from mesh surface +/// +/// Model +/// Cutted shapes +/// Define Triangles of interest. +/// Edge between inside / outside. +/// NOTE: Not const because it need to flag proccessed faces +/// Areas of interest from mesh +CutAOIs create_cut_area_of_interests(const CutMesh &mesh, + const ExPolygons &shapes, + FaceTypeMap &face_type_map); + +} // namespace priv + +void priv::Visitor::intersection_point_detected(std::size_t i_id, + int sdim, + HI h_f, + HI h_e, + const CutMesh &tm_f, + const CutMesh &tm_e, + bool is_target_coplanar, + bool is_source_coplanar) +{ + if (i_id >= intersections.size()) { + size_t capacity = Slic3r::next_highest_power_of_2(i_id + 1); + intersections.reserve(capacity); + intersections.resize(capacity); + } + + const IntersectingElement *intersection_ptr = nullptr; + if (&tm_e == &shape) { + assert(&tm_f == &object); + switch (sdim) { + case 1: + // edge x edge intersection + intersection_ptr = &edge_shape_map[shape.edge(h_e)]; + break; + case 2: + // edge x face intersection + intersection_ptr = &face_shape_map[shape.face(h_e)]; + break; + default: assert(false); + } + if (is_target_coplanar) + vert_shape_map[object.source(h_f)] = intersection_ptr; + if (is_source_coplanar) + vert_shape_map[object.target(h_f)] = intersection_ptr; + } else { + assert(&tm_f == &shape && &tm_e == &object); + assert(!is_target_coplanar); + assert(!is_source_coplanar); + if (is_target_coplanar || is_source_coplanar) + *is_valid = false; + intersection_ptr = &edge_shape_map[shape.edge(h_f)]; + if (sdim == 0) vert_shape_map[object.target(h_e)] = intersection_ptr; + } + + if (intersection_ptr->shape_point_index == std::numeric_limits::max()) { + // there is unexpected intersection + // Top (or Bottom) shape contour edge (or vertex) intersection + // Suggest to change projection min/max limits + *is_valid = false; + } + intersections[i_id] = intersection_ptr; +} + +void priv::Visitor::new_vertex_added(std::size_t i_id, VI v, const CutMesh &tm) +{ + assert(&tm == &object); + assert(i_id < intersections.size()); + const IntersectingElement *intersection_ptr = intersections[i_id]; + assert(intersection_ptr != nullptr); + // intersection was not filled in function intersection_point_detected + //assert(intersection_ptr->point_index != std::numeric_limits::max()); + vert_shape_map[v] = intersection_ptr; +} + +bool priv::is_face_inside(HI hi, + const CutMesh &mesh, + const CutMesh &shape_mesh, + const VertexShapeMap &vertex_shape_map, + const ExPolygonsIndices &shape2index) +{ + VI vi_from = mesh.source(hi); + VI vi_to = mesh.target(hi); + // This face has a constrained edge. + const IntersectingElement &shape_from = *vertex_shape_map[vi_from]; + const IntersectingElement &shape_to = *vertex_shape_map[vi_to]; + assert(shape_from.shape_point_index != std::numeric_limits::max()); + assert(shape_from.attr != (unsigned char) IntersectingElement::Type::undefined); + assert(shape_to.shape_point_index != std::numeric_limits::max()); + assert(shape_to.attr != (unsigned char) IntersectingElement::Type::undefined); + + // index into contour + uint32_t i_from = shape_from.shape_point_index; + uint32_t i_to = shape_to.shape_point_index; + IntersectingElement::Type type_from = shape_from.get_type(); + IntersectingElement::Type type_to = shape_to.get_type(); + if (i_from == i_to && type_from == type_to) { + // intersecting element must be face + assert(type_from == IntersectingElement::Type::face_1 || + type_from == IntersectingElement::Type::face_2); + + // count of vertices is twice as count of point in the contour + uint32_t i = i_from * 2; + // j is next contour point in vertices + uint32_t j = i + 2; + if (shape_from.is_last()) { + ExPolygonsIndex point_id = shape2index.cvt(i_from); + point_id.point_index = 0; + j = shape2index.cvt(point_id)*2; + } + + // opposit point(in triangle face) to edge + const P3 &p = mesh.point(mesh.target(mesh.next(hi))); + + // abc is source triangle face + CGAL::Sign abcp = type_from == IntersectingElement::Type::face_1 ? + CGAL::orientation(shape_mesh.point(VI(i)), + shape_mesh.point(VI(i + 1)), + shape_mesh.point(VI(j)), p) : + // type_from == IntersectingElement::Type::face_2 + CGAL::orientation(shape_mesh.point(VI(j)), + shape_mesh.point(VI(i + 1)), + shape_mesh.point(VI(j + 1)), p); + return abcp == CGAL::POSITIVE; + } else if (i_from < i_to || (i_from == i_to && type_from < type_to)) { + bool is_last = shape_to.is_last() && shape_from.is_first(); + // check continuity of indicies + assert(i_from == i_to || is_last || (i_from + 1) == i_to); + return !is_last; + } else { + assert(i_from > i_to || (i_from == i_to && type_from > type_to)); + bool is_last = shape_to.is_first() && shape_from.is_last(); + // check continuity of indicies + assert(i_from == i_to || is_last || (i_to + 1) == i_from); + return is_last; + } + + assert(false); + return false; +} + +void priv::set_face_type(FaceTypeMap &face_type_map, + const CutMesh &mesh, + const VertexShapeMap &vertex_shape_map, + const EdgeBoolMap &ecm, + const CutMesh &shape_mesh, + const ExPolygonsIndices &shape2index) +{ + for (EI ei : mesh.edges()) { + if (!ecm[ei]) continue; + HI hi = mesh.halfedge(ei); + FI fi = mesh.face(hi); + bool is_inside = is_face_inside(hi, mesh, shape_mesh, vertex_shape_map, shape2index); + face_type_map[fi] = is_inside ? FaceType::inside : FaceType::outside; + HI hi_op = mesh.opposite(hi); + assert(hi_op.is_valid()); + if (!hi_op.is_valid()) continue; + FI fi_op = mesh.face(hi_op); + assert(fi_op.is_valid()); + if (!fi_op.is_valid()) continue; + face_type_map[fi_op] = (!is_inside) ? FaceType::inside : FaceType::outside; + } +} + +priv::CutAOIs priv::cut_from_model(CutMesh &cgal_model, + const ExPolygons &shapes, + CutMesh &cgal_shape, + float projection_ratio, + const ExPolygonsIndices &s2i) +{ + // pointer to edge or face shape_map + VertexShapeMap vert_shape_map = cgal_model.add_property_map(vert_shape_map_name, nullptr).first; + + // detect anomalities in visitor. + bool is_valid = true; + // NOTE: map are created when convert shapes to cgal model + const EdgeShapeMap& edge_shape_map = cgal_shape.property_map(edge_shape_map_name).first; + const FaceShapeMap& face_shape_map = cgal_shape.property_map(face_shape_map_name).first; + Visitor visitor{cgal_model, cgal_shape, edge_shape_map, face_shape_map, vert_shape_map, &is_valid}; + + // a property map containing the constrained-or-not status of each edge + EdgeBoolMap ecm = cgal_model.add_property_map(is_constrained_edge_name, false).first; + const auto &p = CGAL::parameters::visitor(visitor) + .edge_is_constrained_map(ecm) + .throw_on_self_intersection(false); + const auto& q = CGAL::parameters::do_not_modify(true); + CGAL::Polygon_mesh_processing::corefine(cgal_model, cgal_shape, p, q); + + if (!is_valid) return {}; + + FaceTypeMap face_type_map = cgal_model.add_property_map(face_type_map_name, FaceType::not_constrained).first; + + // Select inside and outside face in model + set_face_type(face_type_map, cgal_model, vert_shape_map, ecm, cgal_shape, s2i); +#ifdef DEBUG_OUTPUT_DIR + store(cgal_model, face_type_map, DEBUG_OUTPUT_DIR + "constrained/"); // only debug +#endif // DEBUG_OUTPUT_DIR + + // flood fill the other faces inside the region. + flood_fill_inner(cgal_model, face_type_map); + +#ifdef DEBUG_OUTPUT_DIR + store(cgal_model, face_type_map, DEBUG_OUTPUT_DIR + "filled/", true); // only debug +#endif // DEBUG_OUTPUT_DIR + + // IMPROVE: AOIs area could be created during flood fill + return create_cut_area_of_interests(cgal_model, shapes, face_type_map); +} + +void priv::flood_fill_inner(const CutMesh &mesh, + FaceTypeMap &face_type_map) +{ + std::vector process; + // guess count of connected not constrained triangles + size_t guess_size = 128; + process.reserve(guess_size); + + // check if neighbor(one of three in triangle) has type inside + auto has_inside_neighbor = [&mesh, &face_type_map](FI fi) { + HI hi = mesh.halfedge(fi); + HI hi_end = hi; + auto exist_next = [&hi, &hi_end, &mesh]() -> bool { + hi = mesh.next(hi); + return hi != hi_end; + }; + // loop over 3 half edges of face + do { + HI hi_opposite = mesh.opposite(hi); + // open edge doesn't have opposit half edge + if (!hi_opposite.is_valid()) continue; + FI fi_opposite = mesh.face(hi_opposite); + if (!fi_opposite.is_valid()) continue; + if (face_type_map[fi_opposite] == FaceType::inside) return true; + } while (exist_next()); + return false; + }; + + for (FI fi : mesh.faces()) { + FaceType type = face_type_map[fi]; + if (type != FaceType::not_constrained) continue; + if (!has_inside_neighbor(fi)) continue; + assert(process.empty()); + process.push_back(fi); + //store(mesh, face_type_map, DEBUG_OUTPUT_DIR + "progress.off"); + + while (!process.empty()) { + FI process_fi = process.back(); + process.pop_back(); + // Do not fill twice + FaceType& process_type = face_type_map[process_fi]; + if (process_type == FaceType::inside) continue; + process_type = FaceType::inside; + + // check neighbor triangle + HI hi = mesh.halfedge(process_fi); + HI hi_end = hi; + auto exist_next = [&hi, &hi_end, &mesh]() -> bool { + hi = mesh.next(hi); + return hi != hi_end; + }; + do { + HI hi_opposite = mesh.opposite(hi); + // open edge doesn't have opposit half edge + if (!hi_opposite.is_valid()) continue; + FI fi_opposite = mesh.face(hi_opposite); + if (!fi_opposite.is_valid()) continue; + FaceType type_opposite = face_type_map[fi_opposite]; + if (type_opposite == FaceType::not_constrained) + process.push_back(fi_opposite); + } while (exist_next()); + } + } +} + +void priv::collect_surface_data(std::queue &process, + std::vector &faces, + std::vector &outlines, + FaceTypeMap &face_type_map, + const CutMesh &mesh) +{ + assert(!process.empty()); + assert(faces.empty()); + assert(outlines.empty()); + while (!process.empty()) { + FI fi = process.front(); + process.pop(); + + FaceType &fi_type = face_type_map[fi]; + // Do not process twice + if (fi_type == FaceType::inside_processed) continue; + assert(fi_type == FaceType::inside); + // flag face as processed + fi_type = FaceType::inside_processed; + faces.push_back(fi); + + // check neighbor triangle + HI hi = mesh.halfedge(fi); + HI hi_end = hi; + do { + HI hi_opposite = mesh.opposite(hi); + // open edge doesn't have opposit half edge + if (!hi_opposite.is_valid()) { + outlines.push_back(hi); + hi = mesh.next(hi); + continue; + } + FI fi_opposite = mesh.face(hi_opposite); + if (!fi_opposite.is_valid()) { + outlines.push_back(hi); + hi = mesh.next(hi); + continue; + } + FaceType side = face_type_map[fi_opposite]; + if (side == FaceType::inside) { + process.emplace(fi_opposite); + } else if (side == FaceType::outside) { + // store outlines + outlines.push_back(hi); + } + hi = mesh.next(hi); + } while (hi != hi_end); + } +} + +void priv::create_reduce_map(ReductionMap &reduction_map, const CutMesh &mesh) +{ + const VertexShapeMap &vert_shape_map = mesh.property_map(vert_shape_map_name).first; + const EdgeBoolMap &ecm = mesh.property_map(is_constrained_edge_name).first; + + // check if vertex was made by edge_2 which is diagonal of quad + auto is_reducible_vertex = [&vert_shape_map](VI reduction_from) -> bool { + const IntersectingElement *ie = vert_shape_map[reduction_from]; + if (ie == nullptr) return false; + IntersectingElement::Type type = ie->get_type(); + return type == IntersectingElement::Type::edge_2; + }; + + /// + /// Append reduction or change existing one. + /// + /// HalEdge between outside and inside face. + /// Target vertex will be reduced, source vertex left + /// [[maybe_unused]] &face_type_map, &is_reducible_vertex are need only in debug + auto add_reduction = [&] //&reduction_map, &mesh, &face_type_map, &is_reducible_vertex + (HI hi) { + VI erase = mesh.target(hi); + VI left = mesh.source(hi); + assert(is_reducible_vertex(erase)); + assert(!is_reducible_vertex(left)); + VI &vi = reduction_map[erase]; + // check if it is first add + if (!vi.is_valid()) + reduction_map[erase] = left; + // I have no better rule than take the first + // for decide which reduction will be better + // But it could be use only one of them + }; + + for (EI ei : mesh.edges()) { + if (!ecm[ei]) continue; + HI hi = mesh.halfedge(ei); + VI vi = mesh.target(hi); + if (is_reducible_vertex(vi)) add_reduction(hi); + + HI hi_op = mesh.opposite(hi); + VI vi_op = mesh.target(hi_op); + if (is_reducible_vertex(vi_op)) add_reduction(hi_op); + } +#ifdef DEBUG_OUTPUT_DIR + store(mesh, reduction_map, DEBUG_OUTPUT_DIR + "reduces/"); +#endif // DEBUG_OUTPUT_DIR +} + +priv::CutAOIs priv::create_cut_area_of_interests(const CutMesh &mesh, + const ExPolygons &shapes, + FaceTypeMap &face_type_map) +{ + // IMPROVE: Create better heuristic for count. + size_t faces_per_cut = mesh.faces().size() / shapes.size(); + size_t outlines_per_cut = faces_per_cut / 2; + size_t cuts_per_model = shapes.size() * 2; + + CutAOIs result; + result.reserve(cuts_per_model); + + // It is faster to use one queue for all cuts + std::queue process; + for (FI fi : mesh.faces()) { + if (face_type_map[fi] != FaceType::inside) continue; + + CutAOI cut; + std::vector &faces = cut.first; + std::vector &outlines = cut.second; + + // faces for one surface cut + faces.reserve(faces_per_cut); + // outline for one surface cut + outlines.reserve(outlines_per_cut); + + assert(process.empty()); + // Process queue of faces to separate to surface_cut + process.push(fi); + collect_surface_data(process, faces, outlines, face_type_map, mesh); + assert(!faces.empty()); + assert(!outlines.empty()); + result.emplace_back(std::move(cut)); + } + return result; +} + +namespace priv { + +/// +/// Calculate projection distance of point [in mm] +/// +/// Point to calc distance +/// Index of point on contour +/// Model of cutting shape +/// Ratio for best projection distance +/// Distance of point from best projection +float calc_distance(const P3 &p, + uint32_t pi, + const CutMesh &shapes_mesh, + float projection_ratio); + +} + +float priv::calc_distance(const P3 &p, + uint32_t pi, + const CutMesh &shapes_mesh, + float projection_ratio) +{ + // It is known because shapes_mesh is created inside of private space + VI vi_start(2 * pi); + VI vi_end(2 * pi + 1); + + // Get range for intersection + const P3 &start = shapes_mesh.point(vi_start); + const P3 &end = shapes_mesh.point(vi_end); + + // find index in vector with biggest difference + size_t max_i = 0; + float max_val = 0.f; + for (size_t i = 0; i < 3; i++) { + float val = start[i] - end[i]; + // abs value + if (val < 0.f) val *= -1.f; + if (max_val < val) { + max_val = val; + max_i = i; + } + } + + float from_start = p[max_i] - start[max_i]; + float best_distance = projection_ratio * (end[max_i] - start[max_i]); + return from_start - best_distance; +} + +priv::VDistances priv::calc_distances(const SurfacePatches &patches, + const CutMeshes &models, + const CutMesh &shapes_mesh, + size_t count_shapes_points, + float projection_ratio) +{ + priv::VDistances result(count_shapes_points); + for (const SurfacePatch &patch : patches) { + // map is created during intersection by corefine visitor + const VertexShapeMap &vert_shape_map = + models[patch.model_id].property_map(vert_shape_map_name).first; + uint32_t patch_index = &patch - &patches.front(); + // map is created during patch creation / dividing + const CvtVI2VI& cvt = patch.mesh.property_map(patch_source_name).first; + // for each point on outline + for (const Loop &loop : patch.loops) + for (const VI &vi_patch : loop) { + VI vi_model = cvt[vi_patch]; + if (!vi_model.is_valid()) continue; + const IntersectingElement *ie = vert_shape_map[vi_model]; + if (ie == nullptr) continue; + assert(ie->shape_point_index != std::numeric_limits::max()); + assert(ie->attr != (unsigned char) IntersectingElement::Type::undefined); + uint32_t pi = ie->shape_point_index; + assert(pi <= count_shapes_points); + std::vector &pds = result[pi]; + uint32_t model_index = patch.model_id; + uint32_t aoi_index = patch.aoi_id; + //uint32_t hi_index = &hi - &patch.outline.front(); + const P3 &p = patch.mesh.point(vi_patch); + float distance = calc_distance(p, pi, shapes_mesh, projection_ratio); + pds.push_back({model_index, aoi_index, patch_index, distance}); + } + } + return result; +} + + +#include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/Line.hpp" +// functions for choose_best_distance +namespace priv { + +// euler square size of vector stored in Point +float calc_size_sq(const Point &p); + +// structure to store index and distance together +struct ClosePoint +{ + // index of closest point from another shape + uint32_t index = std::numeric_limits::max(); + // squere distance to index + float dist_sq = std::numeric_limits::max(); +}; + +struct SearchData{ +// IMPROVE: float lines are enough +std::vector lines; +// convert line index into Shape point index +std::vector cvt; +// contain lines from prev point to Point index +AABBTreeIndirect::Tree<2, double> tree; +}; + +SearchData create_search_data(const ExPolygons &shapes, const std::vector& mask); +uint32_t get_closest_point_index(const SearchData &sd, size_t line_idx, const Vec2d &hit_point, const ExPolygons &shapes, const ExPolygonsIndices &s2i); + +// use AABB Tree Lines to find closest point +uint32_t find_closest_point_index(const Point &p, const ExPolygons &shapes, const ExPolygonsIndices &s2i, const std::vector &mask); + +std::pair find_closest_point_pair(const ExPolygons &shapes, const std::vector &done_shapes, const ExPolygonsIndices &s2i, const std::vector &mask); + +// Search for closest projection to wanted distance +const ProjectionDistance *get_closest_projection(const ProjectionDistances &distance, float wanted_distance); + +// fill result around known index inside one polygon +void fill_polygon_distances(const ProjectionDistance &pd, uint32_t index, const ExPolygonsIndex &id, ProjectionDistances & result, const ExPolygon &shape, const VDistances &distances); + +// search for closest projection for expolygon +// choose correct cut by source point +void fill_shape_distances(uint32_t start_index, const ProjectionDistance *start_pd, ProjectionDistances &result, const ExPolygonsIndices &s2i, const ExPolygon &shape, const VDistances &distances); + +// find close points between finished and unfinished ExPolygons +ClosePoint find_close_point(const Point &p, ProjectionDistances &result, std::vector& finished_shapes,const ExPolygonsIndices &s2i, const ExPolygons &shapes); + +} + +float priv::calc_size_sq(const Point &p){ + // NOTE: p.squaredNorm() can't be use due to overflow max int value + return (float) p.x() * p.x() + (float) p.y() * p.y(); +} + +priv::SearchData priv::create_search_data(const ExPolygons &shapes, + const std::vector &mask) +{ + // IMPROVE: Use float precission (it is enough) + SearchData sd; + sd.lines.reserve(mask.size()); + sd.cvt.reserve(mask.size()); + size_t index = 0; + auto add_lines = [&sd, &index, &mask] + (const Polygon &poly) { + Vec2d prev = poly.back().cast(); + bool use_point = mask[index + poly.points.size() - 1]; + for (const Point &p : poly.points) { + if (!use_point) { + use_point = mask[index]; + if (use_point) prev = p.cast(); + } else if (!mask[index]) { + use_point = false; + } else { + Vec2d p_d = p.cast(); + sd.lines.emplace_back(prev, p_d); + sd.cvt.push_back(index); + prev = p_d; + } + ++index; + } + }; + + for (const ExPolygon &shape : shapes) { + add_lines(shape.contour); + for (const Polygon &hole : shape.holes) add_lines(hole); + } + sd.tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(sd.lines); + return sd; +} + +uint32_t priv::get_closest_point_index(const SearchData &sd, + size_t line_idx, + const Vec2d &hit_point, + const ExPolygons &shapes, + const ExPolygonsIndices &s2i) +{ + const Linef &line = sd.lines[line_idx]; + Vec2d dir = line.a - line.b; + Vec2d dir_abs = dir.cwiseAbs(); + // use x coordinate + int i = (dir_abs.x() > dir_abs.y())? 0 :1; + + bool use_index = abs(line.a[i] - hit_point[i]) > + abs(line.b[i] - hit_point[i]); + size_t point_index = sd.cvt[line_idx]; + + // Lambda used only for check result + [[maybe_unused]] auto is_same = [&s2i, &shapes] + (const Vec2d &p, size_t i) -> bool { + auto id = s2i.cvt(i); + const ExPolygon &shape = shapes[id.expolygons_index]; + const Polygon &poly = (id.polygon_index == 0) ? + shape.contour : + shape.holes[id.polygon_index - 1]; + Vec2i p_ = p.cast(); + return p_ == poly[id.point_index]; + }; + + if (use_index) { + assert(is_same(line.b, point_index)); + return point_index; + } + auto id = s2i.cvt(point_index); + if (id.point_index != 0) { + assert(is_same(line.a, point_index - 1)); + return point_index - 1; + } + const ExPolygon &shape = shapes[id.expolygons_index]; + size_t count_polygon_points = (id.polygon_index == 0) ? + shape.contour.size() : + shape.holes[id.polygon_index - 1].size(); + size_t prev_point_index = point_index + (count_polygon_points - 1); + assert(is_same(line.a, prev_point_index)); + // return previous point index + return prev_point_index; +} + +// use AABB Tree Lines +uint32_t priv::find_closest_point_index(const Point &p, + const ExPolygons &shapes, + const ExPolygonsIndices &s2i, + const std::vector &mask) +{ + SearchData sd = create_search_data(shapes, mask); + size_t line_idx = std::numeric_limits::max(); + Vec2d hit_point; + Vec2d p_d = p.cast(); + [[maybe_unused]] double distance_sq = + AABBTreeLines::squared_distance_to_indexed_lines( + sd.lines, sd.tree, p_d, line_idx, hit_point); + assert(distance_sq > 0); + + // IMPROVE: one could use line ratio to find closest point + return get_closest_point_index(sd, line_idx, hit_point, shapes, s2i); +} + +std::pair priv::find_closest_point_pair( + const ExPolygons &shapes, + const std::vector &done_shapes, + const ExPolygonsIndices &s2i, + const std::vector &mask) +{ + assert(mask.size() == s2i.get_count()); + assert(done_shapes.size() == shapes.size()); + std::vector unfinished_mask = mask; // copy + + size_t index = 0; + for (size_t shape_index = 0; shape_index < shapes.size(); shape_index++) { + size_t count = count_points(shapes[shape_index]); + if (done_shapes[shape_index]) { + for (size_t i = 0; i < count; ++i, ++index) + unfinished_mask[index] = false; + } else { + index += count; + } + } + assert(index == s2i.get_count()); + SearchData sd = create_search_data(shapes, unfinished_mask); + + struct ClosestPair + { + size_t finish_idx = std::numeric_limits::max(); + size_t unfinished_line_idx = std::numeric_limits::max(); + Vec2d hit_point = Vec2d(); + double distance_sq = std::numeric_limits::max(); + } cp; + + index = 0; + for (size_t shape_index = 0; shape_index < shapes.size(); shape_index++) { + const ExPolygon shape = shapes[shape_index]; + if (!done_shapes[shape_index]) { + index += count_points(shape); + continue; + } + + auto search_in_polygon = [&index, &cp, &sd, &mask](const Polygon& polygon) { + for (size_t i = 0; i < polygon.size(); ++i, ++index) { + if (mask[index] == false) continue; + Vec2d p_d = polygon[i].cast(); + size_t line_idx = std::numeric_limits::max(); + Vec2d hit_point; + double distance_sq = AABBTreeLines::squared_distance_to_indexed_lines( + sd.lines, sd.tree, p_d, line_idx, hit_point, cp.distance_sq); + if (distance_sq < 0 || + distance_sq >= cp.distance_sq) continue; + assert(line_idx < sd.lines.size()); + cp.distance_sq = distance_sq; + cp.unfinished_line_idx = line_idx; + cp.hit_point = hit_point; + cp.finish_idx = index; + } + }; + search_in_polygon(shape.contour); + for (const Polygon& hole: shape.holes) + search_in_polygon(hole); + } + assert(index == s2i.get_count()); + // check that exists result + if (cp.finish_idx == std::numeric_limits::max()) { + return std::make_pair(std::numeric_limits::max(), + std::numeric_limits::max()); + } + + size_t unfinished_idx = get_closest_point_index(sd, cp.unfinished_line_idx, cp.hit_point, shapes, s2i); + return std::make_pair(cp.finish_idx, unfinished_idx); +} + +const priv::ProjectionDistance *priv::get_closest_projection( + const ProjectionDistances &distance, float wanted_distance) +{ + // minimal distance + float min_d = std::numeric_limits::max(); + const ProjectionDistance *min_pd = nullptr; + for (const ProjectionDistance &pd : distance) { + float d = std::fabs(pd.distance - wanted_distance); + // There should be limit for maximal distance + if (min_d > d) { + min_d = d; + min_pd = &pd; + } + } + return min_pd; +} + +void priv::fill_polygon_distances(const ProjectionDistance &pd, + uint32_t index, + const ExPolygonsIndex &id, + ProjectionDistances &result, + const ExPolygon &shape, + const VDistances &distances) +{ + const Points& points = (id.polygon_index == 0) ? + shape.contour.points : + shape.holes[id.polygon_index - 1].points; + // border of indexes for Polygon + uint32_t first_index = index - id.point_index; + uint32_t last_index = first_index + points.size(); + + uint32_t act_index = index; + const ProjectionDistance* act_pd = &pd; + + // Copy starting pd to result + result[act_index] = pd; + + auto exist_next = [&distances, &act_index, &act_pd, &result] + (uint32_t nxt_index) { + const ProjectionDistance *nxt_pd = get_closest_projection(distances[nxt_index] ,act_pd->distance); + // exist next projection distance ? + if (nxt_pd == nullptr) return false; + + // check no rewrite result + assert(result[nxt_index].aoi_index == std::numeric_limits::max()); + // copy founded projection to result + result[nxt_index] = *nxt_pd; // copy + + // next + act_index = nxt_index; + act_pd = &result[nxt_index]; + return true; + }; + + // last index in circle + uint32_t finish_index = (index == first_index) ? (last_index - 1) : + (index - 1); + // Positive iteration inside polygon + do { + uint32_t nxt_index = act_index + 1; + // close loop of indexes inside of contour + if (nxt_index == last_index) nxt_index = first_index; + // check that exist next + if (!exist_next(nxt_index)) break; + } while (act_index != finish_index); + + // when all results for polygon are set no neccessary to iterate negative + if (act_index == finish_index) return; + + act_index = index; + act_pd = &pd; + // Negative iteration inside polygon + do { + uint32_t nxt_index = (act_index == first_index) ? + (last_index-1) : (act_index - 1); + // When iterate negative it must be split to parts + // and can't iterate in circle + assert(nxt_index != index); + // check that exist next + if (!exist_next(nxt_index)) break; + } while (true); +} + +// IMPROVE: when select distance fill in all distances from Patch +void priv::fill_shape_distances(uint32_t start_index, + const ProjectionDistance *start_pd, + ProjectionDistances &result, + const ExPolygonsIndices &s2i, + const ExPolygon &shape, + const VDistances &distances) +{ + uint32_t expolygons_index = s2i.cvt(start_index).expolygons_index; + uint32_t first_shape_index = s2i.cvt({expolygons_index, 0, 0}); + do { + fill_polygon_distances(*start_pd, start_index, s2i.cvt(start_index),result, shape, distances); + // seaching only inside shape, return index of closed finished point + auto find_close_finished_point = [&first_shape_index, &shape, &result] + (const Point &p) -> ClosePoint { + uint32_t index = first_shape_index; + ClosePoint cp; + auto check_finished_points = [&cp, &result, &index, &p] + (const Points& pts) { + for (const Point &p_ : pts) { + // finished point with some distances + if (result[index].aoi_index == std::numeric_limits::max()) { + ++index; + continue; + } + float distance = calc_size_sq(p_ - p); + if (cp.dist_sq > distance) { + cp.dist_sq = distance; + cp.index = index; + } + ++index; + } + }; + check_finished_points(shape.contour.points); + for (const Polygon &h : shape.holes) + check_finished_points(h.points); + return cp; + }; + + // find next closest pair of points + // (finished + unfinished) in ExPolygon + start_index = std::numeric_limits::max(); // unfinished_index + uint32_t finished_index = std::numeric_limits::max(); + float dist_sq = std::numeric_limits::max(); + + // first index in shape + uint32_t index = first_shape_index; + auto check_unfinished_points = [&index, &result, &distances, &find_close_finished_point, &dist_sq, &start_index, &finished_index] + (const Points& pts) { + for (const Point &p : pts) { + // try find unfinished + if (result[index].aoi_index != + std::numeric_limits::max() || + distances[index].empty()) { + ++index; + continue; + } + ClosePoint cp = find_close_finished_point(p); + if (dist_sq > cp.dist_sq) { + dist_sq = cp.dist_sq; + start_index = index; + finished_index = cp.index; + } + ++index; + } + }; + // for each unfinished points + check_unfinished_points(shape.contour.points); + for (const Polygon &h : shape.holes) + check_unfinished_points(h.points); + } while (start_index != std::numeric_limits::max()); +} + +priv::ClosePoint priv::find_close_point(const Point &p, + ProjectionDistances &result, + std::vector &finished_shapes, + const ExPolygonsIndices &s2i, + const ExPolygons &shapes) +{ + // result + ClosePoint cp; + // for all finished points + for (uint32_t shape_index = 0; shape_index < shapes.size(); ++shape_index) { + if (!finished_shapes[shape_index]) continue; + const ExPolygon &shape = shapes[shape_index]; + uint32_t index = s2i.cvt({shape_index, 0, 0}); + auto find_close_point_in_points = [&p, &cp, &index, &result] + (const Points &pts){ + for (const Point &p_ : pts) { + // Exist result (is finished) ? + if (result[index].aoi_index == + std::numeric_limits::max()) { + ++index; + continue; + } + float distance_sq = calc_size_sq(p - p_); + if (cp.dist_sq > distance_sq) { + cp.dist_sq = distance_sq; + cp.index = index; + } + ++index; + } + }; + find_close_point_in_points(shape.contour.points); + // shape could be inside of another shape's hole + for (const Polygon& h:shape.holes) + find_close_point_in_points(h.points); + } + return cp; +} + +// IMPROVE: when select distance fill in all distances from Patch +priv::ProjectionDistances priv::choose_best_distance( + const VDistances &distances, const ExPolygons &shapes, const Point &start, const ExPolygonsIndices &s2i, const SurfacePatches &patches) +{ + assert(distances.size() == count_points(shapes)); + + // vector of patches for shape + std::vector> shapes_patches(shapes.size()); + for (const SurfacePatch &patch : patches) + shapes_patches[patch.shape_id].push_back(&patch-&patches.front()); + + // collect one closest projection for each outline point + ProjectionDistances result(distances.size()); + + // store info about finished shapes + std::vector finished_shapes(shapes.size(), {false}); + + // wanted distance from ideal projection + // Distances are relative to projection distance + // so first wanted distance is the closest one (ZERO) + float wanted_distance = 0.f; + + std::vector mask_distances(s2i.get_count(), {true}); + for (const auto &d : distances) + if (d.empty()) mask_distances[&d - &distances.front()] = false; + + // Select point from shapes(text contour) which is closest to center (all in 2d) + uint32_t unfinished_index = find_closest_point_index(start, shapes, s2i, mask_distances); + +#ifdef DEBUG_OUTPUT_DIR + Connections connections; + connections.reserve(shapes.size()); + connections.emplace_back(unfinished_index, unfinished_index); +#endif // DEBUG_OUTPUT_DIR + + do { + const ProjectionDistance* pd = get_closest_projection(distances[unfinished_index], wanted_distance); + // selection of closest_id should proove that pd has value + // (functions: get_closest_point_index and find_close_point_in_points) + assert(pd != nullptr); + uint32_t expolygons_index = s2i.cvt(unfinished_index).expolygons_index; + const ExPolygon &shape = shapes[expolygons_index]; + std::vector &shape_patches = shapes_patches[expolygons_index]; + if (shape_patches.size() == 1){ + // Speed up, only one patch so copy distance from patch + uint32_t first_shape_index = s2i.cvt({expolygons_index, 0, 0}); + uint32_t laset_shape_index = first_shape_index + count_points(shape); + for (uint32_t i = first_shape_index; i < laset_shape_index; ++i) { + const ProjectionDistances &pds = distances[i]; + if (pds.empty()) continue; + // check that index belongs to patch + assert(pds.front().patch_index == shape_patches.front()); + result[i] = pds.front(); + if (pds.size() == 1) continue; + + float relative_distance = fabs(result[i].distance - pd->distance); + // patch could contain multiple value for one outline point + // so choose closest to start point + for (uint32_t pds_index = 1; pds_index < pds.size(); ++pds_index) { + // check that index still belongs to same patch + assert(pds[pds_index].patch_index == shape_patches.front()); + float relative_distance2 = fabs(pds[pds_index].distance - pd->distance); + if (relative_distance > relative_distance2) { + relative_distance = relative_distance2; + result[i] = pds[pds_index]; + } + } + } + } else { + // multiple patches for expolygon + // check that exist patch to fill shape + assert(!shape_patches.empty()); + fill_shape_distances(unfinished_index, pd, result, s2i, shape, distances); + } + + finished_shapes[expolygons_index] = true; + // The most close points between finished and unfinished shapes + auto [finished, unfinished] = find_closest_point_pair( + shapes, finished_shapes, s2i, mask_distances); + + // detection of end (best doesn't have value) + if (finished == std::numeric_limits::max()) break; + + assert(unfinished != std::numeric_limits::max()); + const ProjectionDistance &closest_pd = result[finished]; + // check that best_cp is finished and has result + assert(closest_pd.aoi_index != std::numeric_limits::max()); + wanted_distance = closest_pd.distance; + unfinished_index = unfinished; + +#ifdef DEBUG_OUTPUT_DIR + connections.emplace_back(finished, unfinished); +#endif // DEBUG_OUTPUT_DIR + } while (true); //(unfinished_index != std::numeric_limits::max()); +#ifdef DEBUG_OUTPUT_DIR + store(shapes, mask_distances, connections, DEBUG_OUTPUT_DIR + "closest_points.svg"); +#endif // DEBUG_OUTPUT_DIR + return result; +} + +// functions to help 'diff_model' +namespace priv { +const VI default_vi(std::numeric_limits::max()); + +// Keep info about intersection source +struct Source{ HI hi; int sdim=0;}; +using Sources = std::vector; +const std::string vertex_source_map_name = "v:SourceIntersecting"; +using VertexSourceMap = CutMesh::Property_map; + +/// +/// Corefine visitor +/// Store intersection source for vertices of constrained edge of tm1 +/// Must be used with corefine flag no modification of tm2 +/// +struct IntersectionSources +{ + const CutMesh *patch; // patch + const CutMesh *model; // const model + + VertexSourceMap vmap; + + // keep sources from call intersection_point_detected + // until call new_vertex_added + Sources* sources; + + // count intersections + void intersection_point_detected(std::size_t i_id, + int sdim, + HI h_f, + HI h_e, + const CutMesh &tm_f, + const CutMesh &tm_e, + bool is_target_coplanar, + bool is_source_coplanar) + { + Source source; + if (&tm_e == model) { + source = {h_e, sdim}; + // check other CGAL model that is patch + assert(&tm_f == patch); + if (is_target_coplanar) { + assert(sdim == 0); + vmap[tm_f.source(h_f)] = source; + } + if (is_source_coplanar) { + assert(sdim == 0); + vmap[tm_f.target(h_f)] = source; + } + + // clear source to be able check that this intersection source is + // not used any more + if (is_source_coplanar || is_target_coplanar) source = {}; + } else { + source = {h_f, sdim}; + assert(&tm_f == model && &tm_e == patch); + assert(!is_target_coplanar); + assert(!is_source_coplanar); + // if (is_target_coplanar) vmap[tm_e.source(h_e)] = source; + // if (is_source_coplanar) vmap[tm_e.target(h_e)] = source; + // if (sdim == 0) + // vmap[tm_e.target(h_e)] = source; + } + + // By documentation i_id is consecutive. + // check id goes in a row, without skips + assert(sources->size() == i_id); + // add source of intersection + sources->push_back(source); + } + + /// + /// Store VI to intersections by i_id + /// + /// Order number of intersection point + /// New added vertex + /// Affected mesh + void new_vertex_added(std::size_t i_id, VI v, const CutMesh &tm) + { + // check that it is first insertation into item of vmap + assert(!vmap[v].hi.is_valid()); + // check valid addresing into sources + assert(i_id < sources->size()); + // check that source has value + assert(sources->at(i_id).hi.is_valid()); + vmap[v] = sources->at(i_id); + } + + // Not used visitor functions + void before_subface_creations(FI /* f_old */, CutMesh & /* mesh */) {} + void after_subface_created(FI /* f_new */, CutMesh & /* mesh */) {} + void after_subface_creations(CutMesh &) {} + void before_subface_created(CutMesh &) {} + void before_edge_split(HI /* h */, CutMesh & /* tm */) {} + void edge_split(HI /* hnew */, CutMesh & /* tm */) {} + void after_edge_split() {} + void add_retriangulation_edge(HI /* h */, CutMesh & /* tm */) {} +}; + +/// +/// Create map1 and map2 +/// +/// Convert tm1.face to type +/// Corefined mesh +/// Source of intersection +/// Identify constrainde edge +/// Convert tm1.face to type +void create_face_types(FaceTypeMap &map, + const CutMesh &tm1, + const CutMesh &tm2, + const EdgeBoolMap &ecm, + const VertexSourceMap &sources); + +/// +/// Implement 'cut' Minus 'clipper', where clipper is reverse input Volume +/// NOTE: clipper will be modified (corefined by cut) !!! +/// +/// differ from +/// differ what +/// True on succes, otherwise FALSE +bool clip_cut(SurfacePatch &cut, CutMesh clipper); + +BoundingBoxf3 bounding_box(const CutAOI &cut, const CutMesh &mesh); +BoundingBoxf3 bounding_box(const CutMesh &mesh); +BoundingBoxf3 bounding_box(const SurfacePatch &ecut); + +/// +/// Create patch +/// +/// Define patch faces +/// Source of fis +/// NOTE: Need temporary add property map for convert vertices +/// Options to reduce vertices from fis. +/// NOTE: Used for skip vertices made by diagonal edge in rectangle of shape side +/// Patch +SurfacePatch create_surface_patch(const std::vector &fis, + /*const*/ CutMesh &mesh, + const ReductionMap *rmap = nullptr); + +} // namespace priv + +void priv::create_face_types(FaceTypeMap &map, + const CutMesh &tm1, + const CutMesh &tm2, + const EdgeBoolMap &ecm, + const VertexSourceMap &sources) +{ + auto get_intersection_source = [&tm2](const Source& s1, const Source& s2)->FI{ + // when one of sources is face than return it + FI fi1 = tm2.face(s1.hi); + if (s1.sdim == 2) return fi1; + FI fi2 = tm2.face(s2.hi); + if (s2.sdim == 2) return fi2; + // both vertices are made by same source triangle + if (fi1 == fi2) return fi1; + + // when one from sources is edge second one decide side of triangle triangle + HI hi1_opposit = tm2.opposite(s1.hi); + FI fi1_opposit; + if (hi1_opposit.is_valid()) + fi1_opposit = tm2.face(hi1_opposit); + if (fi2 == fi1_opposit) return fi2; + + HI hi2_opposit = tm2.opposite(s2.hi); + FI fi2_opposit; + if (hi2_opposit.is_valid()) + fi2_opposit = tm2.face(hi2_opposit); + if (fi1 == fi2_opposit) return fi1; + if (fi1_opposit.is_valid() && fi1_opposit == fi2_opposit) + return fi1_opposit; + + // when intersection is vertex need loop over neighbor + for (FI fi_around_hi1 : tm2.faces_around_target(s1.hi)) { + for (FI fi_around_hi2 : tm2.faces_around_target(s2.hi)) { + if (fi_around_hi1 == fi_around_hi2) + return fi_around_hi1; + } + } + + // should never rich it + // Exist case when do not know source triangle for decide side of intersection + assert(false); + return FI(); + }; + + for (FI fi : tm1.faces()) map[fi] = FaceType::not_constrained; + for (EI ei1 : tm1.edges()) { + if (!get(ecm, ei1)) continue; + + // get faces from tm1 (f1a + f1b) + HI hi1 = tm1.halfedge(ei1); + assert(hi1.is_valid()); + FI f1a = tm1.face(hi1); + assert(f1a.is_valid()); + HI hi_op = tm1.opposite(hi1); + assert(hi_op.is_valid()); + FI f1b = tm1.face(hi_op); + assert(f1b.is_valid()); + + // get faces from tm2 (f2a + f2b) + VI vi1_source = tm1.source(hi1); + assert(vi1_source.is_valid()); + VI vi1_target = tm1.target(hi1); + assert(vi1_target.is_valid()); + + const Source &s_s = sources[vi1_source]; + const Source &s_t = sources[vi1_target]; + FI fi2 = get_intersection_source(s_s, s_t); + + // in release solve situation that face was NOT deduced + if (!fi2.is_valid()) continue; + + HI hi2 = tm2.halfedge(fi2); + std::array t; + size_t ti =0; + for (VI vi2 : tm2.vertices_around_face(hi2)) + t[ti++] = &tm2.point(vi2); + + // triangle tip from face f1a + VI vi1a_tip = tm1.target(tm1.next(hi1)); + assert(vi1a_tip.is_valid()); + const P3 &p = tm1.point(vi1a_tip); + + // check if f1a is behinde f2a + // inside mean it will be used + // outside will be discarded + if (CGAL::orientation(*t[0], *t[1], *t[2], p) == CGAL::POSITIVE) { + map[f1a] = FaceType::inside; + map[f1b] = FaceType::outside; + } else { + map[f1a] = FaceType::outside; + map[f1b] = FaceType::inside; + } + } +} + +#include +#include +bool priv::clip_cut(SurfacePatch &cut, CutMesh clipper) +{ + CutMesh& tm = cut.mesh; + // create backup for case that there is no intersection + CutMesh backup_copy = tm; + + class ExistIntersectionClipVisitor: public CGAL::Polygon_mesh_processing::Corefinement::Default_visitor + { + bool* exist_intersection; + public: + ExistIntersectionClipVisitor(bool *exist_intersection): exist_intersection(exist_intersection){} + void intersection_point_detected(std::size_t, int , HI, HI, const CutMesh&, const CutMesh&, bool, bool) + { *exist_intersection = true;} + }; + bool exist_intersection = false; + ExistIntersectionClipVisitor visitor{&exist_intersection}; + + // namep parameters for model tm and function clip + const auto &np_tm = CGAL::parameters::visitor(visitor) + .throw_on_self_intersection(false); + + // name parameters for model clipper and function clip + const auto &np_c = CGAL::parameters::throw_on_self_intersection(false); + // Can't use 'do_not_modify', when Ture than clipper has to be closed !! + // .do_not_modify(true); + // .throw_on_self_intersection(false); is set automaticaly by param 'do_not_modify' + // .clip_volume(false); is set automaticaly by param 'do_not_modify' + + bool suc = CGAL::Polygon_mesh_processing::clip(tm, clipper, np_tm, np_c); + + // true if the output surface mesh is manifold. + // If false is returned tm and clipper are only corefined. + assert(suc); + // decide what TODO when can't clip source object !?! + if (!exist_intersection || !suc) { + // TODO: test if cut is fully in or fully out!! + cut.mesh = backup_copy; + return false; + } + return true; +} + +BoundingBoxf3 priv::bounding_box(const CutAOI &cut, const CutMesh &mesh) { + const P3& p_from_cut = mesh.point(mesh.target(mesh.halfedge(cut.first.front()))); + Vec3d min(p_from_cut.x(), p_from_cut.y(), p_from_cut.z()); + Vec3d max = min; + for (FI fi : cut.first) { + for(VI vi: mesh.vertices_around_face(mesh.halfedge(fi))){ + const P3& p = mesh.point(vi); + for (size_t i = 0; i < 3; ++i) { + if (min[i] > p[i]) min[i] = p[i]; + if (max[i] < p[i]) max[i] = p[i]; + } + } + } + return BoundingBoxf3(min, max); +} + +BoundingBoxf3 priv::bounding_box(const CutMesh &mesh) +{ + const P3 &p_from_cut = *mesh.points().begin(); + Vec3d min(p_from_cut.x(), p_from_cut.y(), p_from_cut.z()); + Vec3d max = min; + for (VI vi : mesh.vertices()) { + const P3 &p = mesh.point(vi); + for (size_t i = 0; i < 3; ++i) { + if (min[i] > p[i]) min[i] = p[i]; + if (max[i] < p[i]) max[i] = p[i]; + } + } + return BoundingBoxf3(min, max); +} + +BoundingBoxf3 priv::bounding_box(const SurfacePatch &ecut) { + return bounding_box(ecut.mesh); +} + +priv::SurfacePatch priv::create_surface_patch(const std::vector &fis, + /* const */ CutMesh &mesh, + const ReductionMap *rmap) +{ + auto is_counted = mesh.add_property_map("v:is_counted").first; + uint32_t count_vertices = 0; + if (rmap == nullptr) { + for (FI fi : fis) + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) + if (!is_counted[vi]) { + is_counted[vi] = true; + ++count_vertices; + } + } else { + for (FI fi : fis) + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) { + // Will vertex be reduced? + if ((*rmap)[vi].is_valid()) continue; + if (!is_counted[vi]) { + is_counted[vi] = true; + ++count_vertices; + } + } + } + mesh.remove_property_map(is_counted); + + uint32_t count_faces = fis.size(); + // IMPROVE: Value is greater than neccessary, count edges used twice + uint32_t count_edges = count_faces*3; + + CutMesh cm; + cm.reserve(count_vertices, count_edges, count_faces); + + // vertex conversion function from mesh VI to result VI + CvtVI2VI mesh2result = mesh.add_property_map("v:mesh2result").first; + + if (rmap == nullptr) { + for (FI fi : fis) { + std::array t; + int index = 0; + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) { + VI &vi_cvt = mesh2result[vi]; + if (!vi_cvt.is_valid()) { + vi_cvt = VI(cm.vertices().size()); + cm.add_vertex(mesh.point(vi)); + } + t[index++] = vi_cvt; + } + cm.add_face(t[0], t[1], t[2]); + } + } else { + for (FI fi :fis) { + std::array t; + int index = 0; + bool exist_reduction = false; + for (VI vi : mesh.vertices_around_face(mesh.halfedge(fi))) { + VI vi_r = (*rmap)[vi]; + if (vi_r.is_valid()) { + exist_reduction = true; + vi = vi_r; + } + VI &vi_cvt = mesh2result[vi]; + if (!vi_cvt.is_valid()) { + vi_cvt = VI(cm.vertices().size()); + cm.add_vertex(mesh.point(vi)); + } + t[index++] = vi_cvt; + } + + // prevent add reduced triangle + if (exist_reduction && + (t[0] == t[1] || + t[1] == t[2] || + t[2] == t[0])) + continue; + + cm.add_face(t[0], t[1], t[2]); + } + } + + assert(count_vertices == cm.vertices().size()); + assert((rmap == nullptr && count_faces == cm.faces().size()) || + (rmap != nullptr && count_faces >= cm.faces().size())); + assert(count_edges >= cm.edges().size()); + + // convert VI from this patch to source VI, when exist + CvtVI2VI cvt = cm.add_property_map(patch_source_name).first; + // vi_s .. VertexIndex into mesh (source) + // vi_d .. new VertexIndex in cm (destination) + for (VI vi_s : mesh.vertices()) { + VI vi_d = mesh2result[vi_s]; + if (!vi_d.is_valid()) continue; + cvt[vi_d] = vi_s; + } + mesh.remove_property_map(mesh2result); + return {std::move(cm)}; +} + +// diff_models help functions +namespace priv { + +struct SurfacePatchEx +{ + SurfacePatch patch; + + // flag that part will be deleted + bool full_inside = false; + // flag that Patch could contain more than one part + bool just_cliped = false; +}; +using SurfacePatchesEx = std::vector; + + +using BBS = std::vector; +/// +/// Create bounding boxes for AOI +/// +/// Cutted AOI from models +/// Source points of cuts +/// Bounding boxes +BBS create_bbs(const VCutAOIs &cuts, const CutMeshes &cut_models); + +using Primitive = CGAL::AABB_face_graph_triangle_primitive; +using Traits = CGAL::AABB_traits; +using Ray = EpicKernel::Ray_3; +using Tree = CGAL::AABB_tree; +using Trees = std::vector; +/// +/// Create AABB trees for check when patch is whole inside of model +/// +/// Source for trees +/// trees +Trees create_trees(const CutMeshes &models); + +/// +/// Check whether bounding box has intersection with model +/// +/// Bounding box to check +/// Model to check with +/// All bounding boxes from VCutAOIs +/// Help index into VCutAOIs +/// True when exist bounding boxes intersection +bool has_bb_intersection(const BoundingBoxf3 &bb, + size_t model_index, + const BBS &bbs, + const ModelCut2index &m2i); + +/// +/// Only for model without intersection +/// Use ray (in projection direction) from a point from patch +/// and count intersections: pair .. outside | odd .. inside +/// +/// Patch to check +/// Model converted to AABB tree +/// Define direction of projection +/// True when patch point lay inside of model defined by tree, +/// otherwise FALSE +bool is_patch_inside_of_model(const SurfacePatch &patch, + const Tree &tree, + const Project3d &projection); + +/// +/// Return some shape point index which identify shape +/// NOTE: Used to find expolygon index +/// +/// Used to search source shapes poin +/// +/// shape point index +uint32_t get_shape_point_index(const CutAOI &cut, const CutMesh &model); + +using PatchNumber = CutMesh::Property_map; +/// +/// Separate triangles singned with number n +/// +/// Face indices owned by separate patch +/// Original patch +/// NOTE: Can't be const. For indexing vetices need temporary add property map +/// conversion map +/// Just separated patch +SurfacePatch separate_patch(const std::vector &fis, + /* const*/ SurfacePatch &patch, + const CvtVI2VI &cvt_from); + +/// +/// Separate connected triangles into it's own patches +/// new patches are added to back of input patches +/// +/// index into patches +/// In/Out Patches +void divide_patch(size_t i, SurfacePatchesEx &patches); + +/// +/// Fill outline in patches by open edges +/// +/// Input/Output meshes with open edges +void collect_open_edges(SurfacePatches &patches); + +} // namespace priv + +std::vector priv::create_bbs(const VCutAOIs &cuts, + const CutMeshes &cut_models) +{ + size_t count = 0; + for (const CutAOIs &cut : cuts) count += cut.size(); + + std::vector bbs; + bbs.reserve(count); + for (size_t model_index = 0; model_index < cut_models.size(); ++model_index) { + const CutMesh &cut_model = cut_models[model_index]; + const CutAOIs &cutAOIs = cuts[model_index]; + for (size_t cut_index = 0; cut_index < cutAOIs.size(); ++cut_index) { + const CutAOI &cut = cutAOIs[cut_index]; + bbs.push_back(bounding_box(cut, cut_model)); + } + } + return bbs; +} + + +priv::Trees priv::create_trees(const CutMeshes &models) { + Trees result; + result.reserve(models.size()); + for (const CutMesh &model : models) { + Tree tree; + tree.insert(faces(model).first, faces(model).second, model); + tree.build(); + result.emplace_back(std::move(tree)); + } + return result; +} + +bool priv::has_bb_intersection(const BoundingBoxf3 &bb, + size_t model_index, + const BBS &bbs, + const ModelCut2index &m2i) +{ + const auto&offsets = m2i.get_offsets(); + // for cut index with model_index2 + size_t start = offsets[model_index]; + size_t next = model_index + 1; + size_t end = (next < offsets.size()) ? offsets[next] : m2i.get_count(); + for (size_t bb_index = start; bb_index < end; bb_index++) + if (bb.intersects(bbs[bb_index])) return true; + return false; +} + +bool priv::is_patch_inside_of_model(const SurfacePatch &patch, + const Tree &tree, + const Project3d &projection) +{ + // TODO: Solve model with hole in projection direction !!! + const P3 &a = patch.mesh.point(VI(0)); + Vec3d a_(a.x(), a.y(), a.z()); + Vec3d b_ = projection.project(a_); + P3 b(b_.x(), b_.y(), b_.z()); + + Ray ray_query(a, b); + size_t count = tree.number_of_intersected_primitives(ray_query); + bool is_in = (count % 2) == 1; + + // try opposit direction result should be same, otherwise open model is used + //Vec3f c_ = a_ - (b_ - a_); // opposit direction + //P3 c(c_.x(), c_.y(), c_.z()); + //Ray ray_query2(a, b); + //size_t count2 = tree.number_of_intersected_primitives(ray_query2); + //bool is_in2 = (count2 % 2) == 1; + assert(((tree.number_of_intersected_primitives( + Ray(a, P3(2 * a.x() - b.x(), + 2 * a.y() - b.y(), + 2 * a.z() - b.z()))) % + 2) == 1) == is_in); + return is_in; +} + +uint32_t priv::get_shape_point_index(const CutAOI &cut, const CutMesh &model) +{ + // map is created during intersection by corefine visitor + const VertexShapeMap &vert_shape_map = model.property_map(vert_shape_map_name).first; + // for each half edge of outline + for (HI hi : cut.second) { + VI vi = model.source(hi); + const IntersectingElement *ie = vert_shape_map[vi]; + if (ie == nullptr) continue; + assert(ie->shape_point_index != std::numeric_limits::max()); + return ie->shape_point_index; + } + // can't found any intersecting element in cut + assert(false); + return 0; +} + +priv::SurfacePatch priv::separate_patch(const std::vector& fis, + SurfacePatch &patch, + const CvtVI2VI &cvt_from) +{ + assert(patch.mesh.is_valid()); + SurfacePatch patch_new = create_surface_patch(fis, patch.mesh); + patch_new.bb = bounding_box(patch_new.mesh); + patch_new.aoi_id = patch.aoi_id; + patch_new.model_id = patch.model_id; + patch_new.shape_id = patch.shape_id; + // fix cvt + CvtVI2VI cvt = patch_new.mesh.property_map(patch_source_name).first; + for (VI &vi : cvt) { + if (!vi.is_valid()) continue; + vi = cvt_from[vi]; + } + return patch_new; +} + +void priv::divide_patch(size_t i, SurfacePatchesEx &patches) +{ + SurfacePatchEx &patch_ex = patches[i]; + assert(patch_ex.just_cliped); + patch_ex.just_cliped = false; + + SurfacePatch& patch = patch_ex.patch; + CutMesh& cm = patch.mesh; + assert(!cm.faces().empty()); + std::string patch_number_name = "f:patch_number"; + CutMesh::Property_map is_processed = cm.add_property_map(patch_number_name, false).first; + + const CvtVI2VI& cvt_from = patch.mesh.property_map(patch_source_name).first; + + std::vector fis; + fis.reserve(cm.faces().size()); + + SurfacePatchesEx new_patches; + std::vector queue; + // IMPROVE: create groups around triangles and than connect groups + for (FI fi_cm : cm.faces()) { + if (is_processed[fi_cm]) continue; + assert(queue.empty()); + queue.push_back(fi_cm); + if (!fis.empty()) { + // Be carefull after push to patches, + // all ref on patch contain non valid values + SurfacePatchEx patch_ex_n; + patch_ex_n.patch = separate_patch(fis, patch, cvt_from); + patch_ex_n.patch.is_whole_aoi = false; + new_patches.push_back(std::move(patch_ex_n)); + fis.clear(); + } + // flood fill from triangle fi_cm to surrounding + do { + FI fi_q = queue.back(); + queue.pop_back(); + if (is_processed[fi_q]) continue; + is_processed[fi_q] = true; + fis.push_back(fi_q); + HI hi = cm.halfedge(fi_q); + for (FI fi : cm.faces_around_face(hi)) { + // by documentation The face descriptor may be the null face, and it may be several times the same face descriptor. + if (!fi.is_valid()) continue; + if (!is_processed[fi]) queue.push_back(fi); + } + } while (!queue.empty()); + } + cm.remove_property_map(is_processed); + assert(!fis.empty()); + + // speed up for only one patch - no dividing (the most common) + if (new_patches.empty()) { + patch.bb = bounding_box(cm); + patch.is_whole_aoi = false; + } else { + patch = separate_patch(fis, patch, cvt_from); + patches.insert(patches.end(), new_patches.begin(), new_patches.end()); + } +} + +void priv::collect_open_edges(SurfacePatches &patches) { + std::vector open_half_edges; + for (SurfacePatch &patch : patches) { + open_half_edges.clear(); + const CutMesh &mesh = patch.mesh; + for (FI fi : mesh.faces()) { + HI hi1 = mesh.halfedge(fi); + assert(hi1.is_valid()); + HI hi2 = mesh.next(hi1); + assert(hi2.is_valid()); + HI hi3 = mesh.next(hi2); + assert(hi3.is_valid()); + // Is fi triangle? + assert(mesh.next(hi3) == hi1); + for (HI hi : {hi1, hi2, hi3}) { + HI hi_op = mesh.opposite(hi); + FI fi_op = mesh.face(hi_op); + if (!fi_op.is_valid()) + open_half_edges.push_back(hi); + } + } + patch.loops = create_loops(open_half_edges, mesh); + } +} + +priv::SurfacePatches priv::diff_models(VCutAOIs &cuts, + /*const*/ CutMeshes &cut_models, + /*const*/ CutMeshes &models, + const Project3d &projection) +{ + // IMPROVE: when models contain ONE mesh. It is only about convert cuts to patches + // and reduce unneccessary triangles on contour + + //Convert model_index and cut_index into one index + priv::ModelCut2index m2i(cuts); + + // create bounding boxes for cuts + std::vector bbs = create_bbs(cuts, cut_models); + Trees trees(models.size()); + + SurfacePatches patches; + + // queue of patches for one AOI (permanent with respect to for loop) + SurfacePatchesEx aoi_patches; + + //SurfacePatches aoi_patches; + patches.reserve(m2i.get_count()); // only approximation of count + size_t index = 0; + for (size_t model_index = 0; model_index < models.size(); ++model_index) { + CutAOIs &model_cuts = cuts[model_index]; + CutMesh &cut_model_ = cut_models[model_index]; + const CutMesh &cut_model = cut_model_; + ReductionMap vertex_reduction_map = cut_model_.add_property_map(vertex_reduction_map_name).first; + create_reduce_map(vertex_reduction_map, cut_model); + + for (size_t cut_index = 0; cut_index < model_cuts.size(); ++cut_index, ++index) { + const CutAOI &cut = model_cuts[cut_index]; + SurfacePatchEx patch_ex; + SurfacePatch &patch = patch_ex.patch; + patch = create_surface_patch(cut.first, cut_model_, &vertex_reduction_map); + patch.bb = bbs[index]; + patch.aoi_id = cut_index; + patch.model_id = model_index; + patch.shape_id = get_shape_point_index(cut, cut_model); + patch.is_whole_aoi = true; + + aoi_patches.clear(); + aoi_patches.push_back(patch_ex); + for (size_t model_index2 = 0; model_index2 < models.size(); ++model_index2) { + // do not clip source model itself + if (model_index == model_index2) continue; + for (SurfacePatchEx &patch_ex : aoi_patches) { + SurfacePatch &patch = patch_ex.patch; + if (has_bb_intersection(patch.bb, model_index2, bbs, m2i) && + clip_cut(patch, models[model_index2])){ + patch_ex.just_cliped = true; + } else { + // build tree on demand + // NOTE: it is possible not neccessary: e.g. one model + Tree &tree = trees[model_index2]; + if (tree.empty()) { + const CutMesh &model = models[model_index2]; + auto f_range = faces(model); + tree.insert(f_range.first, f_range.second, model); + tree.build(); + } + if (is_patch_inside_of_model(patch, tree, projection)) + patch_ex.full_inside = true; + } + } + // erase full inside + for (size_t i = aoi_patches.size(); i != 0; --i) { + auto it = aoi_patches.begin() + (i - 1); + if (it->full_inside) aoi_patches.erase(it); + } + + // detection of full AOI inside of model + if (aoi_patches.empty()) break; + + // divide cliped into parts + size_t end = aoi_patches.size(); + for (size_t i = 0; i < end; ++i) + if (aoi_patches[i].just_cliped) + divide_patch(i, aoi_patches); + } + + if (!aoi_patches.empty()) { + patches.reserve(patches.size() + aoi_patches.size()); + for (SurfacePatchEx &patch : aoi_patches) + patches.push_back(std::move(patch.patch)); + + } + } + cut_model_.remove_property_map(vertex_reduction_map); + } + + // Also use outline inside of patches(made by non manifold models) + // IMPROVE: trace outline from AOIs + collect_open_edges(patches); + return patches; +} + +bool priv::is_over_whole_expoly(const SurfacePatch &patch, + const ExPolygons &shapes, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes) +{ + if (!patch.is_whole_aoi) return false; + return is_over_whole_expoly(cutAOIs[patch.model_id][patch.aoi_id], + shapes[patch.shape_id], + meshes[patch.model_id]); +} + +bool priv::is_over_whole_expoly(const CutAOI &cutAOI, + const ExPolygon &shape, + const CutMesh &mesh) +{ + // NonInterupted contour is without other point and contain all from shape + const VertexShapeMap &vert_shape_map = mesh.property_map(vert_shape_map_name).first; + for (HI hi : cutAOI.second) { + const IntersectingElement *ie_s = vert_shape_map[mesh.source(hi)]; + const IntersectingElement *ie_t = vert_shape_map[mesh.target(hi)]; + if (ie_s == nullptr || ie_t == nullptr) + return false; + + assert(ie_s->attr != (unsigned char) IntersectingElement::Type::undefined); + assert(ie_t->attr != (unsigned char) IntersectingElement::Type::undefined); + + // check if it is neighbor indices + uint32_t i_s = ie_s->shape_point_index; + uint32_t i_t = ie_t->shape_point_index; + assert(i_s != std::numeric_limits::max()); + assert(i_t != std::numeric_limits::max()); + if (i_s == std::numeric_limits::max() || + i_t == std::numeric_limits::max()) + return false; + + // made by same index + if (i_s == i_t) continue; + + // order from source to target + if (i_s > i_t) { + std::swap(i_s, i_t); + std::swap(ie_s, ie_t); + } + // Must be after fix order !! + bool is_last_polygon_segment = ie_s->is_first() && ie_t->is_last(); + if (is_last_polygon_segment) { + std::swap(i_s, i_t); + std::swap(ie_s, ie_t); + } + + // Is continous indices + if (!is_last_polygon_segment && + (ie_s->is_last() || (i_s + 1) != i_t)) + return false; + + IntersectingElement::Type t_s = ie_s->get_type(); + IntersectingElement::Type t_t = ie_t->get_type(); + if (t_s == IntersectingElement::Type::undefined || + t_t == IntersectingElement::Type::undefined) + return false; + + // next segment must start with edge intersection + if (t_t != IntersectingElement::Type::edge_1) + return false; + + // After face1 must be edge2 or face2 + if (t_s == IntersectingElement::Type::face_1) + return false; + } + + // When all open edges are on contour than there is NO holes is shape + auto is_open = [&mesh](HI hi)->bool { + HI opposite = mesh.opposite(hi); + return !mesh.face(opposite).is_valid(); + }; + + std::vector opens; // copy + opens.reserve(cutAOI.second.size()); + for (HI hi : cutAOI.second) // from lower to bigger + if (is_open(hi)) opens.push_back(hi); + std::sort(opens.begin(), opens.end()); + + for (FI fi: cutAOI.first) { + HI face_hi = mesh.halfedge(fi); + for (HI hi : mesh.halfedges_around_face(face_hi)) { + if (!is_open(hi)) continue; + // open edge + auto lb = std::lower_bound(opens.begin(), opens.end(), hi); + if (lb == opens.end() || *lb != hi) + return false; // not in contour + } + } + return true; +} + +std::vector priv::select_patches(const ProjectionDistances &best_distances, + const SurfacePatches &patches, + + const ExPolygons &shapes, + const ExPolygonsIndices &s2i, + const VCutAOIs &cutAOIs, + const CutMeshes &meshes, + const Project &projection) +{ + // extension to cover numerical mistake made by back projection patch from 3d to 2d + const float extend_delta = 5.f / Emboss::SHAPE_SCALE; // [Font points scaled by Emboss::SHAPE_SCALE] + + // vector of patches for shape + std::vector> used_shapes_patches(shapes.size()); + std::vector in_distances(patches.size(), {false}); + for (const ProjectionDistance &d : best_distances) { + // exist valid projection for shape point? + if (d.patch_index == std::numeric_limits::max()) continue; + if (in_distances[d.patch_index]) continue; + in_distances[d.patch_index] = true; + + ExPolygonsIndex id = s2i.cvt(&d - &best_distances.front()); + used_shapes_patches[id.expolygons_index].push_back(d.patch_index); + } + + // vector of patches for shape + std::vector> shapes_patches(shapes.size()); + for (const SurfacePatch &patch : patches) + shapes_patches[patch.shape_id].push_back(&patch - &patches.front()); + +#ifdef DEBUG_OUTPUT_DIR + std::string store_dir = DEBUG_OUTPUT_DIR + "select_patches/"; + prepare_dir(store_dir); +#endif // DEBUG_OUTPUT_DIR + + for (size_t shape_index = 0; shape_index < shapes.size(); shape_index++) { + const ExPolygon &shape = shapes[shape_index]; + std::vector &used_shape_patches = used_shapes_patches[shape_index]; + if (used_shape_patches.empty()) continue; + // is used all exist patches? + if (used_shapes_patches.size() == shapes_patches[shape_index].size()) continue; + if (used_shape_patches.size() == 1) { + uint32_t patch_index = used_shape_patches.front(); + const SurfacePatch &patch = patches[patch_index]; + if (is_over_whole_expoly(patch, shapes, cutAOIs, meshes)) continue; + } + + // only shapes containing multiple patches + // or not full filled are back projected (hard processed) + + // intersection of converted patches to 2d + ExPolygons fill; + fill.reserve(used_shape_patches.size()); + + // Heuristics to predict which patch to be used need average patch depth + Vec2d used_patches_depth(std::numeric_limits::max(), std::numeric_limits::min()); + for (uint32_t patch_index : used_shape_patches) { + ExPolygon patch_area = to_expoly(patches[patch_index], projection, used_patches_depth); + //*/ + ExPolygons patch_areas = offset_ex(patch_area, extend_delta); + fill.insert(fill.end(), patch_areas.begin(), patch_areas.end()); + /*/ + // without save extension + fill.push_back(patch_area); + //*/ + } + fill = union_ex(fill); + + // not cutted area of expolygon + ExPolygons rest = diff_ex(ExPolygons{shape}, fill, ApplySafetyOffset::Yes); +#ifdef DEBUG_OUTPUT_DIR + BoundingBox shape_bb = get_extents(shape); + SVG svg(store_dir + "shape_" + std::to_string(shape_index) + ".svg", shape_bb); + svg.draw(fill, "darkgreen"); + svg.draw(rest, "green"); +#endif // DEBUG_OUTPUT_DIR + + // already filled by multiple patches + if (rest.empty()) continue; + + // find patches overlaped rest area + struct PatchShape{ + uint32_t patch_index; + ExPolygon shape; + ExPolygons intersection; + double depth_range_center_distance; // always positive + }; + using PatchShapes = std::vector; + PatchShapes patch_shapes; + + double used_patches_depth_center = (used_patches_depth[0] + used_patches_depth[1]) / 2; + + // sort used_patches for faster search + std::sort(used_shape_patches.begin(), used_shape_patches.end()); + for (uint32_t patch_index : shapes_patches[shape_index]) { + // check is patch already used + auto it = std::lower_bound(used_shape_patches.begin(), used_shape_patches.end(), patch_index); + if (it != used_shape_patches.end() && *it == patch_index) continue; + + // Heuristics to predict which patch to be used need average patch depth + Vec2d patche_depth_range(std::numeric_limits::max(), std::numeric_limits::min()); + ExPolygon patch_shape = to_expoly(patches[patch_index], projection, patche_depth_range); + double depth_center = (patche_depth_range[0] + patche_depth_range[1]) / 2; + double depth_range_center_distance = std::fabs(used_patches_depth_center - depth_center); + + ExPolygons patch_intersection = intersection_ex(ExPolygons{patch_shape}, rest); + if (patch_intersection.empty()) continue; + + patch_shapes.push_back({patch_index, patch_shape, patch_intersection, depth_range_center_distance}); + } + + // nothing to add + if (patch_shapes.empty()) continue; + // only one solution to add + if (patch_shapes.size() == 1) { + used_shape_patches.push_back(patch_shapes.front().patch_index); + continue; + } + + // Idea: Get depth range of used patches and add patches in order by distance to used depth center + std::sort(patch_shapes.begin(), patch_shapes.end(), [](const PatchShape &a, const PatchShape &b) + { return a.depth_range_center_distance < b.depth_range_center_distance; }); + +#ifdef DEBUG_OUTPUT_DIR + for (size_t i = patch_shapes.size(); i > 0; --i) { + const PatchShape &p = patch_shapes[i - 1]; + int gray_level = (i * 200) / patch_shapes.size(); + std::stringstream color; + color << "#" << std::hex << std::setfill('0') << std::setw(2) << gray_level << gray_level << gray_level; + svg.draw(p.shape, color.str()); + Point text_pos = get_extents(p.shape).center().cast(); + svg.draw_text(text_pos, std::to_string(i-1).c_str(), "orange", std::ceil(shape_bb.size().x() / 20 * 0.000001)); + //svg.draw(p.intersection, color.str()); + } +#endif // DEBUG_OUTPUT_DIR + + for (const PatchShape &patch : patch_shapes) { + // Check when exist some place to fill + ExPolygons patch_intersection = intersection_ex(patch.intersection, rest); + if (patch_intersection.empty()) continue; + + // Extend for sure + ExPolygons intersection = offset_ex(patch.intersection, extend_delta); + rest = diff_ex(rest, intersection, ApplySafetyOffset::Yes); + + used_shape_patches.push_back(patch.patch_index); + if (rest.empty()) break; + } + + // QUESTION: How to select which patch to use? How to sort them? + // Now is used back projection distance from used patches + // + // Idealy by outline depth: (need ray cast into patches) + // how to calc wanted depth - idealy by depth of outline help to overlap + // how to calc patch depth - depth in place of outline position + // Which outline to use between + + } + + std::vector result(patches.size(), {false}); + for (const std::vector &patches: used_shapes_patches) + for (uint32_t patch_index : patches) { + assert(patch_index < result.size()); + // check only onece insertation of patch + assert(!result[patch_index]); + result[patch_index] = true; + } + return result; +} + +priv::Loops priv::create_loops(const std::vector &outlines, const CutMesh& mesh) +{ + Loops loops; + Loops unclosed; + for (HI hi : outlines) { + VI vi_s = mesh.source(hi); + VI vi_t = mesh.target(hi); + Loop *loop_move = nullptr; + Loop *loop_connect = nullptr; + for (std::vector &cut : unclosed) { + if (cut.back() != vi_s) continue; + if (cut.front() == vi_t) { + // cut closing + loop_move = &cut; + } else { + loop_connect = &cut; + } + break; + } + if (loop_move != nullptr) { + // index of closed cut + size_t index = loop_move - &unclosed.front(); + // move cut to result + loops.emplace_back(std::move(*loop_move)); + // remove it from unclosed cut + unclosed.erase(unclosed.begin() + index); + } else if (loop_connect != nullptr) { + // try find tail to connect cut + Loop *loop_tail = nullptr; + for (Loop &cut : unclosed) { + if (cut.front() != vi_t) continue; + loop_tail = &cut; + break; + } + if (loop_tail != nullptr) { + // index of tail + size_t index = loop_tail - &unclosed.front(); + // move to connect vector + loop_connect->insert(loop_connect->end(), + make_move_iterator(loop_tail->begin()), + make_move_iterator(loop_tail->end())); + // remove tail from unclosed cut + unclosed.erase(unclosed.begin() + index); + } else { + loop_connect->push_back(vi_t); + } + } else { // not found + bool create_cut = true; + // try to insert to front of cut + for (Loop &cut : unclosed) { + if (cut.front() != vi_t) continue; + cut.insert(cut.begin(), vi_s); + create_cut = false; + break; + } + if (create_cut) + unclosed.emplace_back(std::vector{vi_s, vi_t}); + } + } + assert(unclosed.empty()); + return loops; +} + +Polygons priv::unproject_loops(const SurfacePatch &patch, const Project &projection, Vec2d &depth_range) +{ + assert(!patch.loops.empty()); + if (patch.loops.empty()) return {}; + + // NOTE: this method is working only when patch did not contain outward faces + Polygons polys; + polys.reserve(patch.loops.size()); + // project conture into 2d space to fillconvert outlines to + + size_t count = 0; + for (const Loop &l : patch.loops) count += l.size(); + std::vector depths; + depths.reserve(count); + + Points pts; + for (const Loop &l : patch.loops) { + pts.clear(); + pts.reserve(l.size()); + for (VI vi : l) { + const P3 &p3 = patch.mesh.point(vi); + Vec3d p(p3.x(), p3.y(), p3.z()); + double depth; + std::optional p2_opt = projection.unproject(p, &depth); + if (depth_range[0] > depth) depth_range[0] = depth; // min + if (depth_range[1] < depth) depth_range[1] = depth; // max + // Check when appear that skip is enough for poit which can't be unprojected + // - it could break contour + assert(p2_opt.has_value()); + if (!p2_opt.has_value()) continue; + + pts.push_back(p2_opt->cast()); + depths.push_back(static_cast(depth)); + } + // minimal is triangle + assert(pts.size() >= 3); + if (pts.size() < 3) continue; + + polys.emplace_back(pts); + } + + assert(!polys.empty()); + return polys; +} + +ExPolygon priv::to_expoly(const SurfacePatch &patch, const Project &projection, Vec2d &depth_range) +{ + Polygons polys = unproject_loops(patch, projection, depth_range); + // should not be used when no opposit triangle are counted so should not create overlaps + ClipperLib::PolyFillType fill_type = ClipperLib::PolyFillType::pftEvenOdd; + ExPolygons expolys = Slic3r::union_ex(polys, fill_type); + assert(expolys.size() == 1); + if (expolys.empty()) return {}; + return expolys.front(); +} + +SurfaceCut priv::patch2cut(SurfacePatch &patch) +{ + CutMesh &mesh = patch.mesh; + + std::string convert_map_name = "v:convert"; + CutMesh::Property_map convert_map = + mesh.add_property_map(convert_map_name).first; + + size_t indices_size = mesh.faces().size(); + size_t vertices_size = mesh.vertices().size(); + + SurfaceCut sc; + sc.indices.reserve(indices_size); + sc.vertices.reserve(vertices_size); + for (VI vi : mesh.vertices()) { + // vi order is is not sorted + // assert(vi.idx() == sc.vertices.size()); + // vi is not continous + // assert(vi.idx() < vertices_size); + convert_map[vi] = sc.vertices.size(); + const P3 &p = mesh.point(vi); + sc.vertices.emplace_back(p.x(), p.y(), p.z()); + } + + for (FI fi : mesh.faces()) { + HI hi = mesh.halfedge(fi); + assert(mesh.next(hi).is_valid()); + assert(mesh.next(mesh.next(hi)).is_valid()); + // Is fi triangle? + assert(mesh.next(mesh.next(mesh.next(hi))) == hi); + + // triangle indicies + Vec3i ti; + size_t i = 0; + for (VI vi : { mesh.source(hi), + mesh.target(hi), + mesh.target(mesh.next(hi))}) + ti[i++] = convert_map[vi]; + sc.indices.push_back(ti); + } + + sc.contours.reserve(patch.loops.size()); + for (const Loop &loop : patch.loops) { + sc.contours.push_back({}); + std::vector &contour = sc.contours.back(); + contour.reserve(loop.size()); + for (VI vi : loop) contour.push_back(convert_map[vi]); + } + + // Not neccessary, clean and free memory + mesh.remove_property_map(convert_map); + return sc; +} + +void priv::append(SurfaceCut &sc, SurfaceCut &&sc_add) +{ + if (sc.empty()) { + sc = std::move(sc_add); + return; + } + + if (!sc_add.contours.empty()) { + SurfaceCut::Index offset = static_cast( + sc.vertices.size()); + size_t require = sc.contours.size() + sc_add.contours.size(); + if (sc.contours.capacity() < require) sc.contours.reserve(require); + for (std::vector &cut : sc_add.contours) + for (SurfaceCut::Index &i : cut) i += offset; + Slic3r::append(sc.contours, std::move(sc_add.contours)); + } + its_merge(sc, std::move(sc_add)); +} + +SurfaceCut priv::merge_patches(SurfacePatches &patches, const std::vector& mask) +{ + SurfaceCut result; + for (SurfacePatch &patch : patches) { + size_t index = &patch - &patches.front(); + if (!mask[index]) continue; + append(result, patch2cut(patch)); + } + return result; +} + +#ifdef DEBUG_OUTPUT_DIR +void priv::prepare_dir(const std::string &dir){ + namespace fs = std::filesystem; + if (fs::exists(dir)) { + for (auto &path : fs::directory_iterator(dir)) fs::remove_all(path); + } else { + fs::create_directories(dir); + } +} + +namespace priv{ +int reduction_order = 0; +int filled_order = 0; +int constrained_order = 0; +int diff_patch_order = 0; + +} // namespace priv + +void priv::initialize_store(const std::string& dir) +{ + // clear previous output + prepare_dir(dir); + reduction_order = 0; + filled_order = 0; + constrained_order = 0; + diff_patch_order = 0; +} + +void priv::store(const Vec3f &vertex, + const Vec3f &normal, + const std::string &file, + float size) +{ + int flatten = 20; + size_t min_i = 0; + for (size_t i = 1; i < 3; i++) + if (normal[min_i] > normal[i]) min_i = i; + Vec3f up_ = Vec3f::Zero(); + up_[min_i] = 1.f; + Vec3f side = normal.cross(up_).normalized() * size; + Vec3f up = side.cross(normal).normalized() * size; + + indexed_triangle_set its; + its.vertices.reserve(flatten + 1); + its.indices.reserve(flatten); + + its.vertices.push_back(vertex); + its.vertices.push_back(vertex + up); + size_t max_i = static_cast(flatten); + for (size_t i = 1; i < max_i; i++) { + float angle = i * 2 * M_PI / flatten; + Vec3f v = vertex + sin(angle) * side + cos(angle) * up; + its.vertices.push_back(v); + its.indices.emplace_back(0, i, i + 1); + } + its.indices.emplace_back(0, flatten, 1); + its_write_obj(its, file.c_str()); +} + +void priv::store(const CutMesh &mesh, const FaceTypeMap &face_type_map, const std::string& dir, bool is_filled) +{ + std::string off_file; + if (is_filled) { + if (filled_order == 0) prepare_dir(dir); + off_file = dir + "model" + std::to_string(filled_order++) + ".off"; + }else{ + if (constrained_order == 0) prepare_dir(dir); + off_file = dir + "model" + std::to_string(constrained_order++) + ".off"; + } + + CutMesh &mesh_ = const_cast(mesh); + auto face_colors = mesh_.add_property_map("f:color").first; + for (FI fi : mesh.faces()) { + auto &color = face_colors[fi]; + switch (face_type_map[fi]) { + case FaceType::inside: color = CGAL::Color{100, 250, 100}; break; // light green + case FaceType::inside_processed: color = CGAL::Color{170, 0, 0}; break; // dark red + case FaceType::outside: color = CGAL::Color{100, 0, 100}; break; // purple + case FaceType::not_constrained: color = CGAL::Color{127, 127, 127}; break; // gray + default: color = CGAL::Color{0, 0, 255}; // blue + } + } + CGAL::IO::write_OFF(off_file, mesh); + mesh_.remove_property_map(face_colors); +} + +void priv::store(const ExPolygons &shapes, const std::string &svg_file) { + SVG svg(svg_file); + svg.draw(shapes); +} + +void priv::store(const CutMesh &mesh, const ReductionMap &reduction_map, const std::string& dir) +{ + if (reduction_order == 0) prepare_dir(dir); + std::string off_file = dir + "model" + std::to_string(reduction_order++) + ".off"; + + CutMesh &mesh_ = const_cast(mesh); + auto vertex_colors = mesh_.add_property_map("v:color").first; + // initialize to gray color + for (VI vi: mesh.vertices()) + vertex_colors[vi] = CGAL::Color{127, 127, 127}; + + for (VI reduction_from : mesh.vertices()) { + VI reduction_to = reduction_map[reduction_from]; + if (!reduction_to.is_valid()) continue; + vertex_colors[reduction_from] = CGAL::Color{255, 0, 0}; + vertex_colors[reduction_to] = CGAL::Color{0, 0, 255}; + } + + CGAL::IO::write_OFF(off_file, mesh); + mesh_.remove_property_map(vertex_colors); +} + +namespace priv { +indexed_triangle_set create_indexed_triangle_set(const std::vector &faces, + const CutMesh &mesh); +} // namespace priv + +indexed_triangle_set priv::create_indexed_triangle_set( + const std::vector &faces, const CutMesh &mesh) +{ + std::vector vertices; + vertices.reserve(faces.size() * 2); + + indexed_triangle_set its; + its.indices.reserve(faces.size()); + for (FI fi : faces) { + HI hi = mesh.halfedge(fi); + HI hi_end = hi; + + int ti = 0; + Vec3i t; + + do { + VI vi = mesh.source(hi); + auto res = std::find(vertices.begin(), vertices.end(), vi); + t[ti++] = res - vertices.begin(); + if (res == vertices.end()) vertices.push_back(vi); + hi = mesh.next(hi); + } while (hi != hi_end); + + its.indices.push_back(t); + } + + its.vertices.reserve(vertices.size()); + for (VI vi : vertices) { + const auto &p = mesh.point(vi); + its.vertices.emplace_back(p.x(), p.y(), p.z()); + } + return its; +} + +void priv::store(const CutAOIs &aois, const CutMesh &mesh, const std::string &dir) { + auto create_outline_its = + [&mesh](const std::vector &outlines) -> indexed_triangle_set { + static const float line_width = 0.1f; + indexed_triangle_set its; + its.indices.reserve(2*outlines.size()); + its.vertices.reserve(outlines.size()*4); + for (HI hi : outlines) { + //FI fi = mesh.face(hi); + VI vi_a = mesh.source(hi); + VI vi_b = mesh.target(hi); + VI vi_c = mesh.target(mesh.next(hi)); + P3 p3_a = mesh.point(vi_a); + P3 p3_b = mesh.point(vi_b); + P3 p3_c = mesh.point(vi_c); + + Vec3f a(p3_a.x(), p3_a.y(), p3_a.z()); + Vec3f b(p3_b.x(), p3_b.y(), p3_b.z()); + Vec3f c(p3_c.x(), p3_c.y(), p3_c.z()); + + Vec3f v1 = b - a; // from a to b + v1.normalize(); + Vec3f v2 = c - a; // from a to c + v2.normalize(); + Vec3f norm = v1.cross(v2); + norm.normalize(); + Vec3f perp_to_edge = norm.cross(v1); + perp_to_edge.normalize(); + Vec3f dir = -perp_to_edge * line_width; + + size_t ai = its.vertices.size(); + its.vertices.push_back(a); + size_t bi = its.vertices.size(); + its.vertices.push_back(b); + size_t ai2 = its.vertices.size(); + its.vertices.push_back(a + dir); + size_t bi2 = its.vertices.size(); + its.vertices.push_back(b + dir); + + its.indices.push_back(Vec3i(ai, ai2, bi)); + its.indices.push_back(Vec3i(ai2, bi2, bi)); + } + return its; + }; + + prepare_dir(dir); + for (const auto &aoi : aois) { + size_t index = &aoi - &aois.front(); + std::string file = dir + "aoi" + std::to_string(index) + ".obj"; + indexed_triangle_set its = create_indexed_triangle_set(aoi.first, mesh); + its_write_obj(its, file.c_str()); + + // exist some outline? + if (aoi.second.empty()) continue; + std::string file_outline = dir + "outline" + std::to_string(index) + ".obj"; + indexed_triangle_set outline = create_outline_its(aoi.second); + its_write_obj(outline, file_outline.c_str()); + } +} + +void priv::store(const SurfacePatches &patches, const std::string &dir) { + prepare_dir(dir); + for (const priv::SurfacePatch &patch : patches) { + size_t index = &patch - &patches.front(); + if (patch.mesh.faces().empty()) continue; + CGAL::IO::write_OFF(dir + "patch" + std::to_string(index) + ".off", patch.mesh); + } +} +// +//void priv::store(const ProjectionDistances &pds, +// const VCutAOIs &aois, +// const CutMeshes &meshes, +// const std::string &file, +// float width) +//{ +// // create rectangle for each half edge from projection distances +// indexed_triangle_set its; +// its.vertices.reserve(4 * pds.size()); +// its.indices.reserve(2 * pds.size()); +// for (const ProjectionDistance &pd : pds) { +// if (pd.aoi_index == std::numeric_limits::max()) continue; +// HI hi = aois[pd.model_index][pd.aoi_index].second[pd.hi_index]; +// const CutMesh &mesh = meshes[pd.model_index]; +// VI vi1 = mesh.source(hi); +// VI vi2 = mesh.target(hi); +// VI vi3 = mesh.target(mesh.next(hi)); +// const P3 &p1 = mesh.point(vi1); +// const P3 &p2 = mesh.point(vi2); +// const P3 &p3 = mesh.point(vi3); +// Vec3f v1(p1.x(), p1.y(), p1.z()); +// Vec3f v2(p2.x(), p2.y(), p2.z()); +// Vec3f v3(p3.x(), p3.y(), p3.z()); +// +// Vec3f v12 = v2 - v1; +// v12.normalize(); +// Vec3f v13 = v3 - v1; +// v13.normalize(); +// Vec3f n = v12.cross(v13); +// n.normalize(); +// Vec3f side = n.cross(v12); +// side.normalize(); +// side *= -width; +// +// uint32_t i = its.vertices.size(); +// its.vertices.push_back(v1); +// its.vertices.push_back(v1+side); +// its.vertices.push_back(v2); +// its.vertices.push_back(v2+side); +// +// its.indices.emplace_back(i, i + 1, i + 2); +// its.indices.emplace_back(i + 2, i + 1, i + 3); +// } +// its_write_obj(its, file.c_str()); +//} + +void priv::store(const ExPolygons &shapes, const std::vector &mask, const Connections &connections, const std::string &file_svg) +{ + auto bb = get_extents(shapes); + int width = get_extents(shapes.front()).size().x() / 70; + + SVG svg(file_svg, bb); + svg.draw(shapes); + + ExPolygonsIndices s2i(shapes); + auto get_point = [&shapes, &s2i](size_t i)->Point { + auto id = s2i.cvt(i); + const ExPolygon &s = shapes[id.expolygons_index]; + const Polygon &p = (id.polygon_index == 0) ? + s.contour : + s.holes[id.polygon_index - 1]; + return p[id.point_index]; + }; + + bool is_first = true; + for (const Connection &c : connections) { + if (is_first) { + is_first = false; + Point p = get_point(c.first); + svg.draw(p, "purple", 4 * width); + continue; + } + Point p1 = get_point(c.first); + Point p2 = get_point(c.second); + svg.draw(Line(p1, p2), "red", width); + } + + for (size_t i = 0; i < s2i.get_count(); i++) { + Point p = get_point(i); + svg.draw(p, "black", 2*width); + if (!mask[i]) + svg.draw(p, "white", width); + } + svg.Close(); +} + +namespace priv { +/// +/// Create model consist of rectangles for each contour edge +/// +/// +/// +/// +indexed_triangle_set create_contour_its(const indexed_triangle_set& its, const std::vector &contour); + +/// +/// Getter on triangle tip (third vertex of face) +/// +/// First vertex index +/// Second vertex index +/// Source model +/// Tip Vertex index +unsigned int get_triangle_tip(unsigned int vi1, + unsigned int vi2, + const indexed_triangle_set &its); +} + + +unsigned int priv::get_triangle_tip(unsigned int vi1, + unsigned int vi2, + const indexed_triangle_set &its) +{ + assert(vi1 < its.vertices.size()); + assert(vi2 < its.vertices.size()); + for (const auto &t : its.indices) { + unsigned int tvi = std::numeric_limits::max(); + for (const auto &vi : t) { + unsigned int vi_ = static_cast(vi); + if (vi_ == vi1) continue; + if (vi_ == vi2) continue; + if (tvi == std::numeric_limits::max()) { + tvi = vi_; + } else { + tvi = std::numeric_limits::max(); + break; + } + } + if (tvi != std::numeric_limits::max()) + return tvi; + } + // triangle with indices vi1 and vi2 doesnt exist + assert(false); + return std::numeric_limits::max(); +} + +indexed_triangle_set priv::create_contour_its( + const indexed_triangle_set &its, const std::vector &contour) +{ + static const float line_width = 0.1f; + indexed_triangle_set result; + result.vertices.reserve((contour.size() + 1) * 4); + result.indices.reserve((contour.size() + 1) * 2); + unsigned int prev_vi = contour.back(); + for (unsigned int vi : contour) { + const Vec3f &a = its.vertices[vi]; + const Vec3f &b = its.vertices[prev_vi]; + const Vec3f &c = its.vertices[get_triangle_tip(vi, prev_vi, its)]; + + Vec3f v1 = b - a; // from a to b + v1.normalize(); + Vec3f v2 = c - a; // from a to c + v2.normalize(); + // triangle normal + Vec3f norm = v1.cross(v2); + norm.normalize(); + // perpendiculat to edge lay on triangle + Vec3f perp_to_edge = norm.cross(v1); + perp_to_edge.normalize(); + + Vec3f dir = -perp_to_edge * line_width; + + size_t ai = result.vertices.size(); + result.vertices.push_back(a); + size_t bi = result.vertices.size(); + result.vertices.push_back(b); + size_t ai2 = result.vertices.size(); + result.vertices.push_back(a + dir); + size_t bi2 = result.vertices.size(); + result.vertices.push_back(b + dir); + + result.indices.push_back(Vec3i(ai, bi, ai2)); + result.indices.push_back(Vec3i(ai2, bi, bi2)); + prev_vi = vi; + } + return result; +} + +//void priv::store(const SurfaceCuts &cut, const std::string &dir) { +// prepare_dir(dir); +// for (const auto &c : cut) { +// size_t index = &c - &cut.front(); +// std::string file = dir + "cut" + std::to_string(index) + ".obj"; +// its_write_obj(c, file.c_str()); +// for (const auto& contour : c.contours) { +// size_t c_index = &contour - &c.contours.front(); +// std::string c_file = dir + "cut" + std::to_string(index) + +// "contour" + std::to_string(c_index) + ".obj"; +// indexed_triangle_set c_its = create_contour_its(c, contour); +// its_write_obj(c_its, c_file.c_str()); +// } +// } +//} + +void priv::store(const SurfaceCut &cut, const std::string &file, const std::string &contour_dir) { + prepare_dir(contour_dir); + its_write_obj(cut, file.c_str()); + for (const auto& contour : cut.contours) { + size_t c_index = &contour - &cut.contours.front(); + std::string c_file = contour_dir + std::to_string(c_index) + ".obj"; + indexed_triangle_set c_its = create_contour_its(cut, contour); + its_write_obj(c_its, c_file.c_str()); + } +} + +void priv::store(const std::vector &models, + const std::string &obj_filename) +{ + indexed_triangle_set merged_model; + for (const indexed_triangle_set &model : models) + its_merge(merged_model, model); + its_write_obj(merged_model, obj_filename.c_str()); +} + +void priv::store(const std::vector &models, + const std::string &dir) +{ + prepare_dir(dir); + if (models.empty()) return; + if (models.size() == 1) { + CGAL::IO::write_OFF(dir + "model.off", models.front()); + return; + } + size_t model_index = 0; + for (const priv::CutMesh& model : models) { + std::string filename = dir + "model" + std::to_string(model_index++) + ".off"; + CGAL::IO::write_OFF(filename, model); + } +} + +// store projection center +void priv::store(const Emboss::IProjection &projection, + const Point &point_to_project, + float projection_ratio, + const std::string &obj_filename) +{ + auto [front, back] = projection.create_front_back(point_to_project); + Vec3d diff = back - front; + Vec3d pos = front + diff * projection_ratio; + priv::store(pos.cast(), diff.normalized().cast(), + DEBUG_OUTPUT_DIR + "projection_center.obj"); // only debug +} + +#endif // DEBUG_OUTPUT_DIR + +bool Slic3r::corefine_test(const std::string &model_path, const std::string &shape_path) { + priv::CutMesh model, shape; + if (!CGAL::IO::read_OFF(model_path, model)) return false; + if (!CGAL::IO::read_OFF(shape_path, shape)) return false; + + CGAL::Polygon_mesh_processing::corefine(model, shape); + return true; +} diff --git a/src/libslic3r/CutSurface.hpp b/src/libslic3r/CutSurface.hpp new file mode 100644 index 0000000000..9f4e3159cf --- /dev/null +++ b/src/libslic3r/CutSurface.hpp @@ -0,0 +1,74 @@ +#ifndef slic3r_CutSurface_hpp_ +#define slic3r_CutSurface_hpp_ + +#include +#include // indexed_triangle_set +#include "ExPolygon.hpp" +#include "Emboss.hpp" // IProjection + +namespace Slic3r{ + +/// +/// Represents cutted surface from object +/// Extend index triangle set by outlines +/// +struct SurfaceCut : public indexed_triangle_set +{ + // vertex indices(index to mesh vertices) + using Index = unsigned int; + using Contour = std::vector; + using Contours = std::vector; + // list of circulated open surface + Contours contours; +}; + +/// +/// Cut surface shape from models. +/// +/// Multiple shape to cut from model +/// Multi mesh to cut, need to be in same coordinate system +/// Define transformation 2d shape into 3d +/// Define ideal ratio between front and back projection to cut +/// 0 .. means use closest to front projection +/// 1 .. means use closest to back projection +/// value from <0, 1> +/// +/// Cutted surface from model +SurfaceCut cut_surface(const ExPolygons &shapes, + const std::vector &models, + const Emboss::IProjection &projection, + float projection_ratio); + +/// +/// Create model from surface cuts by projection +/// +/// Surface from model with outlines +/// Way of emboss +/// Mesh +indexed_triangle_set cut2model(const SurfaceCut &cut, + const Emboss::IProject3d &projection); + +/// +/// Separate (A)rea (o)f (I)nterest .. AoI from model +/// NOTE: Only 2d filtration, do not filtrate by Z coordinate +/// +/// Input model +/// Bounding box to project into space +/// Define tranformation of BB into space +/// Triangles lay at least partialy inside of projected Bounding box +indexed_triangle_set its_cut_AoI(const indexed_triangle_set &its, + const BoundingBox &bb, + const Emboss::IProjection &projection); + +/// +/// Separate triangles by mask +/// +/// Input model +/// Mask - same size as its::indices +/// Copy of indices by mask(with their vertices) +indexed_triangle_set its_mask(const indexed_triangle_set &its, const std::vector &mask); + +bool corefine_test(const std::string &model_path, const std::string &shape_path); + +} // namespace Slic3r +#endif // slic3r_CutSurface_hpp_ diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp new file mode 100644 index 0000000000..d7a3ac1f63 --- /dev/null +++ b/src/libslic3r/Emboss.cpp @@ -0,0 +1,1294 @@ +#include "Emboss.hpp" +#include +#include +#include +#include +#include // union_ex + for boldness(polygon extend(offset)) +#include "IntersectionPoints.hpp" + +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "imgui/imstb_truetype.h" // stbtt_fontinfo +#include "Utils.hpp" // ScopeGuard + +#include // CGAL project +#include "libslic3r.h" + +// to heal shape +#include "ExPolygonsIndex.hpp" +#include "libslic3r/AABBTreeLines.hpp" // search structure for found close points +#include "libslic3r/Line.hpp" +#include "libslic3r/BoundingBox.hpp" + +using namespace Slic3r; +// do not expose out of this file stbtt_ data types +namespace priv{ + +bool is_valid(const Emboss::FontFile &font, unsigned int index); +std::optional load_font_info(const unsigned char *data, unsigned int index = 0); +std::optional get_glyph(const stbtt_fontinfo &font_info, int unicode_letter, float flatness); + +// take glyph from cache +const Emboss::Glyph* get_glyph(int unicode, const Emboss::FontFile &font, const FontProp &font_prop, + Emboss::Glyphs &cache, std::optional &font_info_opt); + +EmbossStyle create_style(std::wstring name, std::wstring path); + +// scale and convert float to int coordinate +Point to_point(const stbtt__point &point); + +// helpr for heal shape +bool remove_same_neighbor(Slic3r::Points &points); +bool remove_same_neighbor(Slic3r::Polygons &polygons); +bool remove_same_neighbor(ExPolygons &expolygons); + +// NOTE: expolygons can't contain same_neighbor +Points collect_close_points(const ExPolygons &expolygons, double distance = .6); + +const Points pts_2x2({Point(0, 0), Point(1, 0), Point(1, 1), Point(0, 1)}); +const Points pts_3x3({Point(-1, -1), Point(1, -1), Point(1, 1), Point(-1, 1)}); + +}; + +bool priv::is_valid(const Emboss::FontFile &font, unsigned int index) { + if (font.data == nullptr) return false; + if (font.data->empty()) return false; + if (index >= font.infos.size()) return false; + return true; +} + +std::optional priv::load_font_info( + const unsigned char *data, unsigned int index) +{ + int font_offset = stbtt_GetFontOffsetForIndex(data, index); + if (font_offset < 0) { + assert(false); + // "Font index(" << index << ") doesn't exist."; + return {}; + } + stbtt_fontinfo font_info; + if (stbtt_InitFont(&font_info, data, font_offset) == 0) { + // Can't initialize font. + assert(false); + return {}; + } + return font_info; +} + +bool priv::remove_same_neighbor(Slic3r::Points &points) +{ + if (points.empty()) return false; + auto last = std::unique(points.begin(), points.end()); + if (last == points.end()) return false; + points.erase(last, points.end()); + // clear points without area + if (points.size() <= 2) points.clear(); + return true; +} + +bool priv::remove_same_neighbor(Slic3r::Polygons &polygons) { + if (polygons.empty()) return false; + bool exist = false; + for (Slic3r::Polygon& polygon : polygons) + exist |= remove_same_neighbor(polygon.points); + // remove empty polygons + polygons.erase( + std::remove_if(polygons.begin(), polygons.end(), + [](const Slic3r::Polygon &p) { return p.empty(); }), + polygons.end()); + return exist; +} + +bool priv::remove_same_neighbor(ExPolygons &expolygons) { + if(expolygons.empty()) return false; + bool exist = false; + for (ExPolygon &expoly : expolygons) { + exist |= remove_same_neighbor(expoly.contour.points); + Polygons &holes = expoly.holes; + for (Slic3r::Polygon &hole : holes) + exist |= remove_same_neighbor(hole.points); + // remove empy holes + holes.erase( + std::remove_if(holes.begin(), holes.end(), + [](const Slic3r::Polygon &p) { return p.empty(); }), + holes.end()); + } + // remove empty contours + expolygons.erase( + std::remove_if(expolygons.begin(), expolygons.end(), + [](const ExPolygon &p) { return p.contour.empty(); }), + expolygons.end()); + return exist; +} + +Points priv::collect_close_points(const ExPolygons &expolygons, double distance) { + if (expolygons.empty()) return {}; + if (distance < 0.) return {}; + + // IMPROVE: use int(insted of double) lines and tree + const ExPolygonsIndices ids(expolygons); + const std::vector lines = Slic3r::to_linesf(expolygons, ids.get_count()); + AABBTreeIndirect::Tree<2, double> tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + // Result close points + Points res; + size_t point_index = 0; + auto collect_close = [&res, &point_index, &lines, &tree, &distance, &ids, &expolygons](const Points &pts) { + for (const Point &p : pts) { + Vec2d p_d = p.cast(); + std::vector close_lines = AABBTreeLines::all_lines_in_radius(lines, tree, p_d, distance); + for (size_t index : close_lines) { + // skip point neighbour lines indices + if (index == point_index) continue; + if (&p != &pts.front()) { + if (index == point_index - 1) continue; + } else if (index == (pts.size()-1)) continue; + + // do not doubled side point of segment + const ExPolygonsIndex id = ids.cvt(index); + const ExPolygon &expoly = expolygons[id.expolygons_index]; + const Slic3r::Polygon &poly = id.is_contour() ? expoly.contour : expoly.holes[id.hole_index()]; + const Points &poly_pts = poly.points; + const Point &line_a = poly_pts[id.point_index]; + const Point &line_b = (!ids.is_last_point(id)) ? poly_pts[id.point_index + 1] : poly_pts.front(); + assert(line_a == lines[index].a.cast()); + assert(line_b == lines[index].b.cast()); + if (p == line_a || p == line_b) continue; + res.push_back(p); + } + ++point_index; + } + }; + for (const ExPolygon &expoly : expolygons) { + collect_close(expoly.contour.points); + for (const Slic3r::Polygon &hole : expoly.holes) + collect_close(hole.points); + } + if (res.empty()) return {}; + std::sort(res.begin(), res.end()); + // only unique points + res.erase(std::unique(res.begin(), res.end()), res.end()); + return res; +} + +bool Emboss::divide_segments_for_close_point(ExPolygons &expolygons, double distance) +{ + if (expolygons.empty()) return false; + if (distance < 0.) return false; + + // ExPolygons can't contain same neigbours + priv::remove_same_neighbor(expolygons); + + // IMPROVE: use int(insted of double) lines and tree + const ExPolygonsIndices ids(expolygons); + const std::vector lines = Slic3r::to_linesf(expolygons, ids.get_count()); + AABBTreeIndirect::Tree<2, double> tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + using Div = std::pair; + std::vector
divs; + size_t point_index = 0; + auto check_points = [&divs, &point_index, &lines, &tree, &distance, &ids, &expolygons](const Points &pts) { + for (const Point &p : pts) { + Vec2d p_d = p.cast(); + std::vector close_lines = AABBTreeLines::all_lines_in_radius(lines, tree, p_d, distance); + for (size_t index : close_lines) { + // skip point neighbour lines indices + if (index == point_index) continue; + if (&p != &pts.front()) { + if (index == point_index - 1) continue; + } else if (index == (pts.size()-1)) continue; + + // do not doubled side point of segment + const ExPolygonsIndex id = ids.cvt(index); + const ExPolygon &expoly = expolygons[id.expolygons_index]; + const Slic3r::Polygon &poly = id.is_contour() ? expoly.contour : expoly.holes[id.hole_index()]; + const Points &poly_pts = poly.points; + const Point &line_a = poly_pts[id.point_index]; + const Point &line_b = (!ids.is_last_point(id)) ? poly_pts[id.point_index + 1] : poly_pts.front(); + assert(line_a == lines[index].a.cast()); + assert(line_b == lines[index].b.cast()); + if (p == line_a || p == line_b) continue; + + divs.emplace_back(p, index); + } + ++point_index; + } + }; + for (const ExPolygon &expoly : expolygons) { + check_points(expoly.contour.points); + for (const Slic3r::Polygon &hole : expoly.holes) + check_points(hole.points); + } + + // check if exist division + if (divs.empty()) return false; + + // sort from biggest index to zero + // to be able add points and not interupt indices + std::sort(divs.begin(), divs.end(), + [](const Div &d1, const Div &d2) { return d1.second > d2.second; }); + + auto it = divs.begin(); + // divide close line + while (it != divs.end()) { + // colect division of a line segmen + size_t index = it->second; + auto it2 = it+1; + while (it2 != divs.end() && it2->second == index) ++it2; + + ExPolygonsIndex id = ids.cvt(index); + ExPolygon &expoly = expolygons[id.expolygons_index]; + Slic3r::Polygon &poly = id.is_contour() ? expoly.contour : expoly.holes[id.hole_index()]; + Points &pts = poly.points; + size_t count = it2 - it; + + // add points into polygon to divide in place of near point + if (count == 1) { + pts.insert(pts.begin() + id.point_index + 1, it->first); + ++it; + } else { + // collect points to add into polygon + Points points; + points.reserve(count); + for (; it < it2; ++it) + points.push_back(it->first); + + // need sort by line direction + const Linef &line = lines[index]; + Vec2d dir = line.b - line.a; + // select mayorit direction + int axis = (abs(dir.x()) > abs(dir.y())) ? 0 : 1; + using Fnc = std::function; + Fnc fnc = (dir[axis] < 0) ? Fnc([axis](const Point &p1, const Point &p2) { return p1[axis] > p2[axis]; }) : + Fnc([axis](const Point &p1, const Point &p2) { return p1[axis] < p2[axis]; }) ; + std::sort(points.begin(), points.end(), fnc); + + // use only unique points + points.erase(std::unique(points.begin(), points.end()), points.end()); + + // divide line by adding points into polygon + pts.insert(pts.begin() + id.point_index + 1, + points.begin(), points.end()); + } + assert(it == it2); + } + return true; +} + +ExPolygons Emboss::heal_shape(const Polygons &shape) { + // When edit this code check that font 'ALIENATE.TTF' and glyph 'i' still work + // fix of self intersections + // http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Functions/SimplifyPolygon.htm + ClipperLib::Paths paths = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(shape), ClipperLib::pftNonZero); + const double clean_distance = 1.415; // little grater than sqrt(2) + ClipperLib::CleanPolygons(paths, clean_distance); + Polygons polygons = to_polygons(paths); + + // Do not remove all duplicits but do it better way + // Overlap all duplicit points by rectangle 3x3 + Points duplicits = collect_duplications(to_points(polygons)); + if (!duplicits.empty()) { + polygons.reserve(polygons.size() + duplicits.size()); + for (const Point &p : duplicits) { + Slic3r::Polygon rect_3x3(priv::pts_3x3); + rect_3x3.translate(p); + polygons.push_back(rect_3x3); + } + } + + // TrueTypeFonts use non zero winding number + // https://docs.microsoft.com/en-us/typography/opentype/spec/ttch01 + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html + ExPolygons res = Slic3r::union_ex(polygons, ClipperLib::pftNonZero); + heal_shape(res); + return res; +} + +bool Emboss::heal_shape(ExPolygons &shape, unsigned max_iteration) +{ + if (shape.empty()) return true; + + Slic3r::Polygons holes; + while (--max_iteration) { + priv::remove_same_neighbor(shape); + + Pointfs intersections = intersection_points(shape); + Points duplicits = collect_duplications(to_points(shape)); + //Points close = priv::collect_close_points(shape, 1.); + if (intersections.empty() && duplicits.empty() /* && close.empty() */) break; + + holes.clear(); + holes.reserve(intersections.size() + duplicits.size() /* + close.size()*/); + + // Fix self intersection in result by subtracting hole 2x2 + for (const Vec2d &p : intersections) { + Slic3r::Polygon hole(priv::pts_2x2); + Point tr(std::floor(p.x()), std::floor(p.y())); + hole.translate(tr); + holes.push_back(hole); + } + + // fix duplicit points by hole 3x3 around duplicit point + for (const Point &p : duplicits) { + Slic3r::Polygon hole(priv::pts_3x3); + hole.translate(p); + holes.push_back(hole); + } + + // fix close points in simmilar way as duplicits + //for (const Point &p : close) { + // Slic3r::Polygon hole(priv::pts_3x3); + // hole.translate(p); + // holes.push_back(hole); + //} + + holes = Slic3r::union_(holes); + shape = Slic3r::diff_ex(shape, holes, ApplySafetyOffset::Yes); + } + /* VISUALIZATION of BAD symbols for debug + { + double svg_scale = Emboss::SHAPE_SCALE / unscale(1.); + BoundingBox bb(to_points(shape)); + //bb.scale(svg_scale); + SVG svg("C:/data/temp/fix_self_intersection.svg", bb); + svg.draw(shape); + // svg.draw(polygons, "orange"); + svg.draw(shape, "green"); + + svg.draw(duplicits, "lightgray", 13 / Emboss::SHAPE_SCALE); + Points duplicits3 = collect_duplications(to_points(shape)); + svg.draw(duplicits3, "black", 7 / Emboss::SHAPE_SCALE); + + Pointfs pts2 = intersection_points(shape); + Points pts_i; pts_i.reserve(pts2.size()); + for (auto p : pts2) pts_i.push_back(p.cast()); + svg.draw(pts_i, "red", 8 / Emboss::SHAPE_SCALE); + } //*/ + + if (max_iteration == 0) { + assert(false); + BoundingBox bb = get_extents(shape); + Point size = bb.size(); + if (size.x() < 10) bb.max.x() += 10; + if (size.y() < 10) bb.max.y() += 10; + + Polygon rect({// CCW + bb.min, + {bb.max.x(), bb.min.y()}, + bb.max, + {bb.min.x(), bb.max.y()}}); + + Point offset = bb.size() * 0.1; + Polygon hole({// CW + bb.min + offset, + {bb.min.x() + offset.x(), bb.max.y() - offset.y()}, + bb.max - offset, + {bb.max.x() - offset.x(), bb.min.y() + offset.y()}}); + // BAD symbol + shape = {ExPolygon(rect, hole)}; + return false; + } + + assert(intersection_points(shape).empty()); + assert(collect_duplications(to_points(shape)).empty()); + return true; +} + +std::optional priv::get_glyph(const stbtt_fontinfo &font_info, int unicode_letter, float flatness) +{ + int glyph_index = stbtt_FindGlyphIndex(&font_info, unicode_letter); + if (glyph_index == 0) { + //wchar_t wchar = static_cast(unicode_letter); + //<< "Character unicode letter (" + //<< "decimal value = " << std::dec << unicode_letter << ", " + //<< "hexadecimal value = U+" << std::hex << unicode_letter << std::dec << ", " + //<< "wchar value = " << wchar + //<< ") is NOT defined inside of the font. \n"; + return {}; + } + + Emboss::Glyph glyph; + stbtt_GetGlyphHMetrics(&font_info, glyph_index, &glyph.advance_width, &glyph.left_side_bearing); + + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(&font_info, glyph_index, &vertices); + if (num_verts <= 0) return glyph; // no shape + ScopeGuard sg1([&vertices]() { free(vertices); }); + + int *contour_lengths = NULL; + int num_countour_int = 0; + stbtt__point *points = stbtt_FlattenCurves(vertices, num_verts, + flatness, &contour_lengths, &num_countour_int, font_info.userdata); + if (!points) return glyph; // no valid flattening + ScopeGuard sg2([&contour_lengths, &points]() { + free(contour_lengths); + free(points); + }); + + size_t num_contour = static_cast(num_countour_int); + Polygons glyph_polygons; + glyph_polygons.reserve(num_contour); + size_t pi = 0; // point index + for (size_t ci = 0; ci < num_contour; ++ci) { + int length = contour_lengths[ci]; + // check minimal length for triangle + if (length < 4) { + // weird font + pi+=length; + continue; + } + // last point is first point + --length; + Points pts; + pts.reserve(length); + for (int i = 0; i < length; ++i) + pts.emplace_back(to_point(points[pi++])); + + // last point is first point --> closed contour + assert(pts.front() == to_point(points[pi])); + ++pi; + + // change outer cw to ccw and inner ccw to cw order + std::reverse(pts.begin(), pts.end()); + glyph_polygons.emplace_back(pts); + } + if (!glyph_polygons.empty()) + glyph.shape = Emboss::heal_shape(glyph_polygons); + return glyph; +} + +const Emboss::Glyph* priv::get_glyph( + int unicode, + const Emboss::FontFile & font, + const FontProp & font_prop, + Emboss::Glyphs & cache, + std::optional &font_info_opt) +{ + // TODO: Use resolution by printer configuration, or add it into FontProp + const float RESOLUTION = 0.0125f; // [in mm] + auto glyph_item = cache.find(unicode); + if (glyph_item != cache.end()) return &glyph_item->second; + + unsigned int font_index = font_prop.collection_number.has_value()? + *font_prop.collection_number : 0; + if (!is_valid(font, font_index)) return nullptr; + + if (!font_info_opt.has_value()) { + + font_info_opt = priv::load_font_info(font.data->data(), font_index); + // can load font info? + if (!font_info_opt.has_value()) return nullptr; + } + + float flatness = font.infos[font_index].ascent * RESOLUTION / font_prop.size_in_mm; + + // Fix for very small flatness because it create huge amount of points from curve + if (flatness < RESOLUTION) flatness = RESOLUTION; + + std::optional glyph_opt = + priv::get_glyph(*font_info_opt, unicode, flatness); + + // IMPROVE: multiple loadig glyph without data + // has definition inside of font? + if (!glyph_opt.has_value()) return nullptr; + + if (font_prop.char_gap.has_value()) + glyph_opt->advance_width += *font_prop.char_gap; + + // scale glyph size + glyph_opt->advance_width = + static_cast(glyph_opt->advance_width / Emboss::SHAPE_SCALE); + glyph_opt->left_side_bearing = + static_cast(glyph_opt->left_side_bearing / Emboss::SHAPE_SCALE); + + if (!glyph_opt->shape.empty()) { + if (font_prop.boldness.has_value()) { + float delta = *font_prop.boldness / Emboss::SHAPE_SCALE / + font_prop.size_in_mm; + glyph_opt->shape = Slic3r::union_ex(offset_ex(glyph_opt->shape, delta)); + } + if (font_prop.skew.has_value()) { + const float &ratio = *font_prop.skew; + auto skew = [&ratio](Slic3r::Polygon &polygon) { + for (Slic3r::Point &p : polygon.points) { + p.x() += p.y() * ratio; + } + }; + for (ExPolygon &expolygon : glyph_opt->shape) { + skew(expolygon.contour); + for (Slic3r::Polygon &hole : expolygon.holes) skew(hole); + } + } + } + auto it = cache.insert({unicode, std::move(*glyph_opt)}); + assert(it.second); + return &it.first->second; +} + +EmbossStyle priv::create_style(std::wstring name, std::wstring path) { + return { boost::nowide::narrow(name.c_str()), + boost::nowide::narrow(path.c_str()), + EmbossStyle::Type::file_path, FontProp() }; +} + +Point priv::to_point(const stbtt__point &point) { + return Point(static_cast(std::round(point.x / Emboss::SHAPE_SCALE)), + static_cast(std::round(point.y / Emboss::SHAPE_SCALE))); +} + +#ifdef _WIN32 +#include +#include +#include +#include + +// Get system font file path +std::optional Emboss::get_font_path(const std::wstring &font_face_name) +{ +// static const LPWSTR fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; + static const LPCWSTR fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; + HKEY hKey; + LONG result; + + // Open Windows font registry key + result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, fontRegistryPath, 0, KEY_READ, &hKey); + if (result != ERROR_SUCCESS) return {}; + + DWORD maxValueNameSize, maxValueDataSize; + result = RegQueryInfoKey(hKey, 0, 0, 0, 0, 0, 0, 0, &maxValueNameSize, &maxValueDataSize, 0, 0); + if (result != ERROR_SUCCESS) return {}; + + DWORD valueIndex = 0; + LPWSTR valueName = new WCHAR[maxValueNameSize]; + LPBYTE valueData = new BYTE[maxValueDataSize]; + DWORD valueNameSize, valueDataSize, valueType; + std::wstring wsFontFile; + + // Look for a matching font name + do { + wsFontFile.clear(); + valueDataSize = maxValueDataSize; + valueNameSize = maxValueNameSize; + + result = RegEnumValue(hKey, valueIndex, valueName, &valueNameSize, 0, &valueType, valueData, &valueDataSize); + + valueIndex++; + if (result != ERROR_SUCCESS || valueType != REG_SZ) { + continue; + } + + std::wstring wsValueName(valueName, valueNameSize); + + // Found a match + if (_wcsnicmp(font_face_name.c_str(), wsValueName.c_str(), font_face_name.length()) == 0) { + + wsFontFile.assign((LPWSTR)valueData, valueDataSize); + break; + } + }while (result != ERROR_NO_MORE_ITEMS); + + delete[] valueName; + delete[] valueData; + + RegCloseKey(hKey); + + if (wsFontFile.empty()) return {}; + + // Build full font file path + WCHAR winDir[MAX_PATH]; + GetWindowsDirectory(winDir, MAX_PATH); + + std::wstringstream ss; + ss << winDir << "\\Fonts\\" << wsFontFile; + wsFontFile = ss.str(); + + return wsFontFile; +} + +EmbossStyles Emboss::get_font_list() +{ + //EmbossStyles list1 = get_font_list_by_enumeration(); + //EmbossStyles list2 = get_font_list_by_register(); + //EmbossStyles list3 = get_font_list_by_folder(); + return get_font_list_by_register(); +} + +EmbossStyles Emboss::get_font_list_by_register() { +// static const LPWSTR fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; + static const LPCWSTR fontRegistryPath = L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"; + HKEY hKey; + LONG result; + + // Open Windows font registry key + result = RegOpenKeyEx(HKEY_LOCAL_MACHINE, fontRegistryPath, 0, KEY_READ, &hKey); + if (result != ERROR_SUCCESS) { + assert(false); + //std::wcerr << L"Can not Open register key (" << fontRegistryPath << ")" + // << L", function 'RegOpenKeyEx' return code: " << result << std::endl; + return {}; + } + + DWORD maxValueNameSize, maxValueDataSize; + result = RegQueryInfoKey(hKey, 0, 0, 0, 0, 0, 0, 0, &maxValueNameSize, + &maxValueDataSize, 0, 0); + if (result != ERROR_SUCCESS) { + assert(false); + // Can not earn query key, function 'RegQueryInfoKey' return code: result + return {}; + } + + // Build full font file path + WCHAR winDir[MAX_PATH]; + GetWindowsDirectory(winDir, MAX_PATH); + std::wstring font_path = std::wstring(winDir) + L"\\Fonts\\"; + + EmbossStyles font_list; + DWORD valueIndex = 0; + // Look for a matching font name + LPWSTR font_name = new WCHAR[maxValueNameSize]; + LPBYTE fileTTF_name = new BYTE[maxValueDataSize]; + DWORD font_name_size, fileTTF_name_size, valueType; + do { + fileTTF_name_size = maxValueDataSize; + font_name_size = maxValueNameSize; + + result = RegEnumValue(hKey, valueIndex, font_name, &font_name_size, 0, + &valueType, fileTTF_name, &fileTTF_name_size); + valueIndex++; + if (result != ERROR_SUCCESS || valueType != REG_SZ) continue; + std::wstring font_name_w(font_name, font_name_size); + std::wstring file_name_w((LPWSTR) fileTTF_name, fileTTF_name_size); + std::wstring path_w = font_path + file_name_w; + + // filtrate .fon from lists + size_t pos = font_name_w.rfind(L" (TrueType)"); + if (pos >= font_name_w.size()) continue; + // remove TrueType text from name + font_name_w = std::wstring(font_name_w, 0, pos); + font_list.emplace_back(priv::create_style(font_name_w, path_w)); + } while (result != ERROR_NO_MORE_ITEMS); + delete[] font_name; + delete[] fileTTF_name; + + RegCloseKey(hKey); + return font_list; +} + +// TODO: Fix global function +bool CALLBACK EnumFamCallBack(LPLOGFONT lplf, + LPNEWTEXTMETRIC lpntm, + DWORD FontType, + LPVOID aFontList) +{ + std::vector *fontList = + (std::vector *) (aFontList); + if (FontType & TRUETYPE_FONTTYPE) { + std::wstring name = lplf->lfFaceName; + fontList->push_back(name); + } + return true; + // UNREFERENCED_PARAMETER(lplf); + UNREFERENCED_PARAMETER(lpntm); +} + +EmbossStyles Emboss::get_font_list_by_enumeration() { + + HDC hDC = GetDC(NULL); + std::vector font_names; + EnumFontFamilies(hDC, (LPCTSTR) NULL, (FONTENUMPROC) EnumFamCallBack, + (LPARAM) &font_names); + + EmbossStyles font_list; + for (const std::wstring &font_name : font_names) { + font_list.emplace_back(priv::create_style(font_name, L"")); + } + return font_list; +} + +EmbossStyles Emboss::get_font_list_by_folder() { + EmbossStyles result; + WCHAR winDir[MAX_PATH]; + UINT winDir_size = GetWindowsDirectory(winDir, MAX_PATH); + std::wstring search_dir = std::wstring(winDir, winDir_size) + L"\\Fonts\\"; + WIN32_FIND_DATA fd; + HANDLE hFind; + // By https://en.wikipedia.org/wiki/TrueType has also suffix .tte + std::vector suffixes = {L"*.ttf", L"*.ttc", L"*.tte"}; + for (const std::wstring &suffix : suffixes) { + hFind = ::FindFirstFile((search_dir + suffix).c_str(), &fd); + if (hFind == INVALID_HANDLE_VALUE) continue; + do { + // skip folder . and .. + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; + std::wstring file_name(fd.cFileName); + // TODO: find font name instead of filename + result.emplace_back(priv::create_style(file_name, search_dir + file_name)); + } while (::FindNextFile(hFind, &fd)); + ::FindClose(hFind); + } + return result; +} + +#else +EmbossStyles Emboss::get_font_list() { + // not implemented + return {}; +} + +std::optional Emboss::get_font_path(const std::wstring &font_face_name){ + // not implemented + return {}; +} +#endif + +std::unique_ptr Emboss::create_font_file( + std::unique_ptr> data) +{ + int collection_size = stbtt_GetNumberOfFonts(data->data()); + // at least one font must be inside collection + if (collection_size < 1) { + assert(false); + // There is no font collection inside font data + return nullptr; + } + + unsigned int c_size = static_cast(collection_size); + std::vector infos; + infos.reserve(c_size); + for (unsigned int i = 0; i < c_size; ++i) { + auto font_info = priv::load_font_info(data->data(), i); + if (!font_info.has_value()) return nullptr; + + const stbtt_fontinfo *info = &(*font_info); + // load information about line gap + int ascent, descent, linegap; + stbtt_GetFontVMetrics(info, &ascent, &descent, &linegap); + + float pixels = 1000.; // value is irelevant + float em_pixels = stbtt_ScaleForMappingEmToPixels(info, pixels); + int units_per_em = static_cast(std::round(pixels / em_pixels)); + + infos.emplace_back(FontFile::Info{ascent, descent, linegap, units_per_em}); + } + return std::make_unique(std::move(data), std::move(infos)); +} + +std::unique_ptr Emboss::create_font_file(const char *file_path) +{ + FILE *file = std::fopen(file_path, "rb"); + if (file == nullptr) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Couldn't open " << file_path << " for reading."; + return nullptr; + } + ScopeGuard sg([&file]() { std::fclose(file); }); + + // find size of file + if (fseek(file, 0L, SEEK_END) != 0) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Couldn't fseek file " << file_path << " for size measure."; + return nullptr; + } + size_t size = ftell(file); + if (size == 0) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Size of font file is zero. Can't read."; + return nullptr; + } + rewind(file); + auto buffer = std::make_unique>(size); + size_t count_loaded_bytes = fread((void *) &buffer->front(), 1, size, file); + if (count_loaded_bytes != size) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Different loaded(from file) data size."; + return nullptr; + } + return create_font_file(std::move(buffer)); +} + + +#ifdef _WIN32 +static bool load_hfont(void* hfont, DWORD &dwTable, DWORD &dwOffset, size_t& size, HDC hdc = nullptr){ + bool del_hdc = false; + if (hdc == nullptr) { + del_hdc = true; + hdc = ::CreateCompatibleDC(NULL); + if (hdc == NULL) return false; + } + + // To retrieve the data from the beginning of the file for TrueType + // Collection files specify 'ttcf' (0x66637474). + dwTable = 0x66637474; + dwOffset = 0; + + ::SelectObject(hdc, hfont); + size = ::GetFontData(hdc, dwTable, dwOffset, NULL, 0); + if (size == GDI_ERROR) { + // HFONT is NOT TTC(collection) + dwTable = 0; + size = ::GetFontData(hdc, dwTable, dwOffset, NULL, 0); + } + + if (size == 0 || size == GDI_ERROR) { + if (del_hdc) ::DeleteDC(hdc); + return false; + } + return true; +} + +void * Emboss::can_load(HFONT hfont) +{ + DWORD dwTable=0, dwOffset=0; + size_t size = 0; + if (!load_hfont(hfont, dwTable, dwOffset, size)) return nullptr; + return hfont; +} + +std::unique_ptr Emboss::create_font_file(HFONT hfont) +{ + HDC hdc = ::CreateCompatibleDC(NULL); + if (hdc == NULL) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Can't create HDC by CreateCompatibleDC(NULL)."; + return nullptr; + } + + DWORD dwTable=0,dwOffset = 0; + size_t size; + if (!load_hfont(hfont, dwTable, dwOffset, size, hdc)) { + ::DeleteDC(hdc); + return nullptr; + } + auto buffer = std::make_unique>(size); + size_t loaded_size = ::GetFontData(hdc, dwTable, dwOffset, buffer->data(), size); + ::DeleteDC(hdc); + if (size != loaded_size) { + assert(false); + BOOST_LOG_TRIVIAL(error) << "Different loaded(from HFONT) data size."; + return nullptr; + } + return create_font_file(std::move(buffer)); +} +#endif // _WIN32 + +std::optional Emboss::letter2glyph(const FontFile &font, + unsigned int font_index, + int letter, + float flatness) +{ + if (!priv::is_valid(font, font_index)) return {}; + auto font_info_opt = priv::load_font_info(font.data->data(), font_index); + if (!font_info_opt.has_value()) return {}; + return priv::get_glyph(*font_info_opt, letter, flatness); +} + +ExPolygons Emboss::text2shapes(FontFileWithCache &font_with_cache, + const char *text, + const FontProp &font_prop, + std::function was_canceled) +{ + assert(font_with_cache.has_value()); + std::optional font_info_opt; + Point cursor(0, 0); + ExPolygons result; + const FontFile& font = *font_with_cache.font_file; + unsigned int font_index = font_prop.collection_number.has_value()? + *font_prop.collection_number : 0; + if (!priv::is_valid(font, font_index)) return {}; + const FontFile::Info& info = font.infos[font_index]; + Emboss::Glyphs& cache = *font_with_cache.cache; + std::wstring ws = boost::nowide::widen(text); + for (wchar_t wc: ws){ + if (wc == '\n') { + int line_height = info.ascent - info.descent + info.linegap; + if (font_prop.line_gap.has_value()) + line_height += *font_prop.line_gap; + line_height = static_cast(line_height / SHAPE_SCALE); + + cursor.x() = 0; + cursor.y() -= line_height; + continue; + } + if (wc == '\t') { + // '\t' = 4*space => same as imgui + const int count_spaces = 4; + const Glyph* space = priv::get_glyph(int(' '), font, font_prop, cache, font_info_opt); + if (space == nullptr) continue; + cursor.x() += count_spaces * space->advance_width; + continue; + } + if (wc == '\r') continue; + + int unicode = static_cast(wc); + // check cancelation only before unknown symbol - loading of symbol could be timeconsuming on slow computer and dificult fonts + auto it = cache.find(unicode); + if (it == cache.end() && was_canceled != nullptr && was_canceled()) return {}; + const Glyph *glyph_ptr = (it != cache.end())? &it->second : + priv::get_glyph(unicode, font, font_prop, cache, font_info_opt); + if (glyph_ptr == nullptr) continue; + + // move glyph to cursor position + ExPolygons expolygons = glyph_ptr->shape; // copy + for (ExPolygon &expolygon : expolygons) + expolygon.translate(cursor); + + cursor.x() += glyph_ptr->advance_width; + expolygons_append(result, std::move(expolygons)); + } + result = Slic3r::union_ex(result); + heal_shape(result); + return result; +} + +void Emboss::apply_transformation(const FontProp &font_prop, + Transform3d &transformation) +{ + if (font_prop.angle.has_value()) { + double angle_z = *font_prop.angle; + transformation *= Eigen::AngleAxisd(angle_z, Vec3d::UnitZ()); + } + if (font_prop.distance.has_value()) { + Vec3d translate = Vec3d::UnitZ() * (*font_prop.distance); + transformation.translate(translate); + } +} + +bool Emboss::is_italic(const FontFile &font, unsigned int font_index) +{ + if (font_index >= font.infos.size()) return false; + std::optional font_info_opt = priv::load_font_info(font.data->data(), font_index); + + if (!font_info_opt.has_value()) return false; + stbtt_fontinfo *info = &(*font_info_opt); + + // https://docs.microsoft.com/cs-cz/typography/opentype/spec/name + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html + // 2 ==> Style / Subfamily name + int name_id = 2; + int length; + const char* value = stbtt_GetFontNameString(info, &length, + STBTT_PLATFORM_ID_MICROSOFT, + STBTT_MS_EID_UNICODE_BMP, + STBTT_MS_LANG_ENGLISH, + name_id); + + // value is big endian utf-16 i need extract only normal chars + std::string value_str; + value_str.reserve(length / 2); + for (int i = 1; i < length; i += 2) + value_str.push_back(value[i]); + + // lower case + std::transform(value_str.begin(), value_str.end(), value_str.begin(), + [](unsigned char c) { return std::tolower(c); }); + + const std::vector italics({"italic", "oblique"}); + for (const std::string &it : italics) { + if (value_str.find(it) != std::string::npos) { + return true; + } + } + return false; +} + +std::string Emboss::create_range_text(const std::string &text, + const FontFile &font, + unsigned int font_index, + bool *exist_unknown) +{ + if (!priv::is_valid(font, font_index)) return {}; + + std::wstring ws = boost::nowide::widen(text); + + // need remove symbols not contained in font + std::sort(ws.begin(), ws.end()); + + auto font_info_opt = priv::load_font_info(font.data->data(), 0); + if (!font_info_opt.has_value()) return {}; + const stbtt_fontinfo *font_info = &(*font_info_opt); + + if (exist_unknown != nullptr) *exist_unknown = false; + int prev_unicode = -1; + ws.erase(std::remove_if(ws.begin(), ws.end(), + [&prev_unicode, font_info, exist_unknown](wchar_t wc) -> bool { + int unicode = static_cast(wc); + + // skip white spaces + if (unicode == '\n' || + unicode == '\r' || + unicode == '\t') return true; + + // is duplicit? + if (prev_unicode == unicode) return true; + prev_unicode = unicode; + + // can find in font? + bool is_unknown = !stbtt_FindGlyphIndex(font_info, unicode); + if (is_unknown && exist_unknown != nullptr) + *exist_unknown = true; + return is_unknown; + }), ws.end()); + + return boost::nowide::narrow(ws); +} + +double Emboss::get_shape_scale(const FontProp &fp, const FontFile &ff) +{ + const auto &cn = fp.collection_number; + unsigned int font_index = (cn.has_value()) ? *cn : 0; + int unit_per_em = ff.infos[font_index].unit_per_em; + double scale = fp.size_in_mm / unit_per_em; + // Shape is scaled for store point coordinate as integer + return scale * Emboss::SHAPE_SCALE; +} + +namespace priv { + +void add_quad(uint32_t i1, + uint32_t i2, + indexed_triangle_set &result, + uint32_t count_point) +{ + // bottom indices + uint32_t i1_ = i1 + count_point; + uint32_t i2_ = i2 + count_point; + result.indices.emplace_back(i2, i2_, i1); + result.indices.emplace_back(i1_, i1, i2_); +}; + +indexed_triangle_set polygons2model_unique( + const ExPolygons &shape2d, + const Emboss::IProjection &projection, + const Points &points) +{ + // CW order of triangle indices + std::vector shape_triangles=Triangulation::triangulate(shape2d, points); + uint32_t count_point = points.size(); + + indexed_triangle_set result; + result.vertices.reserve(2 * count_point); + std::vector &front_points = result.vertices; // alias + std::vector back_points; + back_points.reserve(count_point); + + for (const Point &p : points) { + auto p2 = projection.create_front_back(p); + front_points.push_back(p2.first.cast()); + back_points.push_back(p2.second.cast()); + } + + // insert back points, front are already in + result.vertices.insert(result.vertices.end(), + std::make_move_iterator(back_points.begin()), + std::make_move_iterator(back_points.end())); + result.indices.reserve(shape_triangles.size() * 2 + points.size() * 2); + // top triangles - change to CCW + for (const Vec3i &t : shape_triangles) + result.indices.emplace_back(t.x(), t.z(), t.y()); + // bottom triangles - use CW + for (const Vec3i &t : shape_triangles) + result.indices.emplace_back(t.x() + count_point, + t.y() + count_point, + t.z() + count_point); + + // quads around - zig zag by triangles + size_t polygon_offset = 0; + auto add_quads = [&polygon_offset,&result, &count_point] + (const Slic3r::Polygon& polygon) { + uint32_t polygon_points = polygon.points.size(); + // previous index + uint32_t prev = polygon_offset + polygon_points - 1; + for (uint32_t p = 0; p < polygon_points; ++p) { + uint32_t index = polygon_offset + p; + add_quad(prev, index, result, count_point); + prev = index; + } + polygon_offset += polygon_points; + }; + + for (const ExPolygon &expolygon : shape2d) { + add_quads(expolygon.contour); + for (const Slic3r::Polygon &hole : expolygon.holes) add_quads(hole); + } + + return result; +} + +indexed_triangle_set polygons2model_duplicit( + const ExPolygons &shape2d, + const Emboss::IProjection &projection, + const Points &points, + const Points &duplicits) +{ + // CW order of triangle indices + std::vector changes = Triangulation::create_changes(points, duplicits); + std::vector shape_triangles = Triangulation::triangulate(shape2d, points, changes); + uint32_t count_point = *std::max_element(changes.begin(), changes.end()) + 1; + + indexed_triangle_set result; + result.vertices.reserve(2 * count_point); + std::vector &front_points = result.vertices; + std::vector back_points; + back_points.reserve(count_point); + + uint32_t max_index = std::numeric_limits::max(); + for (uint32_t i = 0; i < changes.size(); ++i) { + uint32_t index = changes[i]; + if (max_index != std::numeric_limits::max() && + index <= max_index) continue; // duplicit point + assert(index == max_index + 1); + assert(front_points.size() == index); + assert(back_points.size() == index); + max_index = index; + const Point &p = points[i]; + auto p2 = projection.create_front_back(p); + front_points.push_back(p2.first.cast()); + back_points.push_back(p2.second.cast()); + } + assert(max_index+1 == count_point); + + // insert back points, front are already in + result.vertices.insert(result.vertices.end(), + std::make_move_iterator(back_points.begin()), + std::make_move_iterator(back_points.end())); + + result.indices.reserve(shape_triangles.size() * 2 + points.size() * 2); + // top triangles - change to CCW + for (const Vec3i &t : shape_triangles) + result.indices.emplace_back(t.x(), t.z(), t.y()); + // bottom triangles - use CW + for (const Vec3i &t : shape_triangles) + result.indices.emplace_back(t.x() + count_point, t.y() + count_point, + t.z() + count_point); + + // quads around - zig zag by triangles + size_t polygon_offset = 0; + auto add_quads = [&polygon_offset, &result, count_point, &changes] + (const Slic3r::Polygon &polygon) { + uint32_t polygon_points = polygon.points.size(); + // previous index + uint32_t prev = changes[polygon_offset + polygon_points - 1]; + for (uint32_t p = 0; p < polygon_points; ++p) { + uint32_t index = changes[polygon_offset + p]; + if (prev == index) continue; + add_quad(prev, index, result, count_point); + prev = index; + } + polygon_offset += polygon_points; + }; + + for (const ExPolygon &expolygon : shape2d) { + add_quads(expolygon.contour); + for (const Slic3r::Polygon &hole : expolygon.holes) add_quads(hole); + } + return result; +} +} // namespace priv + +indexed_triangle_set Emboss::polygons2model(const ExPolygons &shape2d, + const IProjection &projection) +{ + Points points = to_points(shape2d); + Points duplicits = collect_duplications(points); + return (duplicits.empty()) ? + priv::polygons2model_unique(shape2d, projection, points) : + priv::polygons2model_duplicit(shape2d, projection, points, duplicits); +} + +std::pair Emboss::ProjectZ::create_front_back(const Point &p) const +{ + Vec3d front( + p.x() * SHAPE_SCALE, + p.y() * SHAPE_SCALE, + 0.); + return std::make_pair(front, project(front)); +} + +Vec3d Emboss::ProjectZ::project(const Vec3d &point) const +{ + Vec3d res = point; // copy + res.z() = m_depth; + return res; +} + +std::optional Emboss::ProjectZ::unproject(const Vec3d &p, double *depth) const { + if (depth != nullptr) *depth /= SHAPE_SCALE; + return Vec2d(p.x() / SHAPE_SCALE, p.y() / SHAPE_SCALE); +} + +Transform3d Emboss::create_transformation_onto_surface(const Vec3f &position, + const Vec3f &normal, + float up_limit) +{ + // up and emboss direction for generated model + Vec3d text_up_dir = Vec3d::UnitY(); + Vec3d text_emboss_dir = Vec3d::UnitZ(); + + // wanted up direction of result + Vec3d wanted_up_side = Vec3d::UnitZ(); + if (std::fabs(normal.z()) > up_limit) wanted_up_side = Vec3d::UnitY(); + + Vec3d wanted_emboss_dir = normal.cast(); + // after cast from float it needs to be normalized again + wanted_emboss_dir.normalize(); + + // create perpendicular unit vector to surface triangle normal vector + // lay on surface of triangle and define up vector for text + Vec3d wanted_up_dir = wanted_emboss_dir + .cross(wanted_up_side) + .cross(wanted_emboss_dir); + // normal3d is NOT perpendicular to normal_up_dir + wanted_up_dir.normalize(); + + // perpendicular to emboss vector of text and normal + Vec3d axis_view; + double angle_view; + if (wanted_emboss_dir == -Vec3d::UnitZ()) { + // text_emboss_dir has opposit direction to wanted_emboss_dir + axis_view = Vec3d::UnitY(); + angle_view = M_PI; + } else { + axis_view = text_emboss_dir.cross(wanted_emboss_dir); + angle_view = std::acos(text_emboss_dir.dot(wanted_emboss_dir)); // in rad + axis_view.normalize(); + } + + Eigen::AngleAxis view_rot(angle_view, axis_view); + Vec3d wanterd_up_rotated = view_rot.matrix().inverse() * wanted_up_dir; + wanterd_up_rotated.normalize(); + double angle_up = std::acos(text_up_dir.dot(wanterd_up_rotated)); + + // text_view and text_view2 should have same direction + Vec3d text_view2 = text_up_dir.cross(wanterd_up_rotated); + Vec3d diff_view = text_emboss_dir - text_view2; + if (std::fabs(diff_view.x()) > 1. || + std::fabs(diff_view.y()) > 1. || + std::fabs(diff_view.z()) > 1.) // oposit direction + angle_up *= -1.; + + Eigen::AngleAxis up_rot(angle_up, text_emboss_dir); + + Transform3d transform = Transform3d::Identity(); + transform.translate(position.cast()); + transform.rotate(view_rot); + transform.rotate(up_rot); + return transform; +} + + +// OrthoProject + +std::pair Emboss::OrthoProject::create_front_back(const Point &p) const { + Vec3d front(p.x(), p.y(), 0.); + Vec3d front_tr = m_matrix * front; + return std::make_pair(front_tr, project(front_tr)); +} + +Vec3d Emboss::OrthoProject::project(const Vec3d &point) const +{ + return point + m_direction; +} + +std::optional Emboss::OrthoProject::unproject(const Vec3d &p, double *depth) const +{ + Vec3d pp = m_matrix_inv * p; + if (depth != nullptr) *depth = pp.z(); + return Vec2d(pp.x(), pp.y()); +} \ No newline at end of file diff --git a/src/libslic3r/Emboss.hpp b/src/libslic3r/Emboss.hpp new file mode 100644 index 0000000000..e188d3fe6e --- /dev/null +++ b/src/libslic3r/Emboss.hpp @@ -0,0 +1,349 @@ +#ifndef slic3r_Emboss_hpp_ +#define slic3r_Emboss_hpp_ + +#include +#include +#include +#include +#include // indexed_triangle_set +#include "Polygon.hpp" +#include "ExPolygon.hpp" +#include "TextConfiguration.hpp" + +namespace Slic3r { + +/// +/// class with only static function add ability to engraved OR raised +/// text OR polygons onto model surface +/// +namespace Emboss +{ + // every glyph's shape point is divided by SHAPE_SCALE - increase precission of fixed point value + // stored in fonts (to be able represents curve by sequence of lines) + static constexpr double SHAPE_SCALE = 0.001; // SCALING_FACTOR promile is fine enough + + /// + /// Collect fonts registred inside OS + /// + /// OS registred TTF font files(full path) with names + EmbossStyles get_font_list(); +#ifdef _WIN32 + EmbossStyles get_font_list_by_register(); + EmbossStyles get_font_list_by_enumeration(); + EmbossStyles get_font_list_by_folder(); +#endif + + /// + /// OS dependent function to get location of font by its name descriptor + /// + /// Unique identificator for font + /// File path to font when found + std::optional get_font_path(const std::wstring &font_face_name); + + // description of one letter + struct Glyph + { + // NOTE: shape is scaled by SHAPE_SCALE + // to be able store points without floating points + ExPolygons shape; + + // values are in font points + int advance_width=0, left_side_bearing=0; + }; + // cache for glyph by unicode + using Glyphs = std::map; + + /// + /// keep information from file about font + /// (store file data itself) + /// + cache data readed from buffer + /// + struct FontFile + { + // loaded data from font file + // must store data size for imgui rasterization + // To not store data on heap and To prevent unneccesary copy + // data are stored inside unique_ptr + std::unique_ptr> data; + + struct Info + { + // vertical position is "scale*(ascent - descent + lineGap)" + int ascent, descent, linegap; + + // for convert font units to pixel + int unit_per_em; + }; + // info for each font in data + std::vector infos; + + FontFile(std::unique_ptr> data, + std::vector &&infos) + : data(std::move(data)), infos(std::move(infos)) + { + assert(this->data != nullptr); + assert(!this->data->empty()); + } + + bool operator==(const FontFile &other) const { + if (data->size() != other.data->size()) + return false; + //if(*data != *other.data) return false; + for (size_t i = 0; i < infos.size(); i++) + if (infos[i].ascent != other.infos[i].ascent || + infos[i].descent == other.infos[i].descent || + infos[i].linegap == other.infos[i].linegap) + return false; + return true; + } + }; + + /// + /// Add caching for shape of glyphs + /// + struct FontFileWithCache + { + // Pointer on data of the font file + std::shared_ptr font_file; + + // Cache for glyph shape + // IMPORTANT: accessible only in plater job thread !!! + // main thread only clear cache by set to another shared_ptr + std::shared_ptr cache; + + FontFileWithCache() : font_file(nullptr), cache(nullptr) {} + FontFileWithCache(std::unique_ptr font_file) + : font_file(std::move(font_file)) + , cache(std::make_shared()) + {} + bool has_value() const { return font_file != nullptr && cache != nullptr; } + }; + + /// + /// Load font file into buffer + /// + /// Location of .ttf or .ttc font file + /// Font object when loaded. + std::unique_ptr create_font_file(const char *file_path); + // data = raw file data + std::unique_ptr create_font_file(std::unique_ptr> data); +#ifdef _WIN32 + // fix for unknown pointer HFONT + using HFONT = void*; + void * can_load(HFONT hfont); + std::unique_ptr create_font_file(HFONT hfont); +#endif // _WIN32 + + /// + /// convert letter into polygons + /// + /// Define fonts + /// Index of font in collection + /// One character defined by unicode codepoint + /// Precision of lettter outline curve in conversion to lines + /// inner polygon cw(outer ccw) + std::optional letter2glyph(const FontFile &font, unsigned int font_index, int letter, float flatness); + + /// + /// Convert text into polygons + /// + /// Define fonts + cache, which could extend + /// Characters to convert + /// User defined property of the font + /// Way to interupt processing + /// Inner polygon cw(outer ccw) + ExPolygons text2shapes(FontFileWithCache &font, const char *text, const FontProp &font_prop, std::function was_canceled = nullptr); + + /// + /// Fix intersections and self intersections in polygons glyph shape + /// + /// Input shape to heal + /// Healed shapes + ExPolygons heal_shape(const Polygons &shape); + + /// + /// NOTE: call Slic3r::union_ex before this call + /// + /// Heal (read: Fix) issues in expolygons: + /// - self intersections + /// - duplicit points + /// - points close to line segments + /// + /// In/Out shape to heal + /// Heal could create another issue, + /// After healing it is checked again until shape is good or maximal count of iteration + /// True when shapes is good otherwise False + bool heal_shape(ExPolygons &shape, unsigned max_iteration = 10); + + /// + /// Divide line segments in place near to point + /// (which could lead to self intersection due to preccision) + /// Remove same neighbors + /// Note: Possible part of heal shape + /// + /// Expolygon to edit + /// (epsilon)Euclidean distance from point to line which divide line + /// True when some division was made otherwise false + bool divide_segments_for_close_point(ExPolygons &expolygons, double distance); + + /// + /// Use data from font property to modify transformation + /// + /// Z-move as surface distance(FontProp::distance) + /// Z-rotation as angle to Y axis(FontProp::angle) + /// In / Out transformation to modify by property + void apply_transformation(const FontProp &font_prop, Transform3d &transformation); + + /// + /// Read information from naming table of font file + /// search for italic (or oblique), bold italic (or bold oblique) + /// + /// Selector of font + /// Index of font in collection + /// True when the font description contains italic/obligue otherwise False + bool is_italic(const FontFile &font, unsigned int font_index); + + /// + /// Create unique character set from string with filtered from text with only character from font + /// + /// Source vector of glyphs + /// Font descriptor + /// Define font in collection + /// True when text contain glyph unknown in font + /// Unique set of character from text contained in font + std::string create_range_text(const std::string &text, const FontFile &font, unsigned int font_index, bool* exist_unknown = nullptr); + + /// + /// Calculate scale for glyph shape convert from shape points to mm + /// + /// Property of font + /// Font data + /// Conversion to mm + double get_shape_scale(const FontProp &fp, const FontFile &ff); + + /// + /// Project spatial point + /// + class IProject3d + { + public: + virtual ~IProject3d() = default; + /// + /// Move point with respect to projection direction + /// e.g. Orthogonal projection will move with point by direction + /// e.g. Spherical projection need to use center of projection + /// + /// Spatial point coordinate + /// Projected spatial point + virtual Vec3d project(const Vec3d &point) const = 0; + }; + + /// + /// Project 2d point into space + /// Could be plane, sphere, cylindric, ... + /// + class IProjection : public IProject3d + { + public: + virtual ~IProjection() = default; + + /// + /// convert 2d point to 3d points + /// + /// 2d coordinate + /// + /// first - front spatial point + /// second - back spatial point + /// + virtual std::pair create_front_back(const Point &p) const = 0; + + /// + /// Back projection + /// + /// Point to project + /// [optional] Depth of 2d projected point. Be careful number is in 2d scale + /// Uprojected point when it is possible + virtual std::optional unproject(const Vec3d &p, double * depth = nullptr) const = 0; + }; + + /// + /// Create triangle model for text + /// + /// text or image + /// Define transformation from 2d to 3d(orientation, position, scale, ...) + /// Projected shape into space + indexed_triangle_set polygons2model(const ExPolygons &shape2d, const IProjection& projection); + + /// + /// Create transformation for emboss text object to lay on surface point + /// + /// Position of surface point + /// Normal of surface point + /// Is compared with normal.z to suggest up direction + /// Transformation onto surface point + Transform3d create_transformation_onto_surface( + const Vec3f &position, const Vec3f &normal, float up_limit = 0.9f); + + class ProjectZ : public IProjection + { + public: + ProjectZ(double depth) : m_depth(depth) {} + // Inherited via IProject + std::pair create_front_back(const Point &p) const override; + Vec3d project(const Vec3d &point) const override; + std::optional unproject(const Vec3d &p, double * depth = nullptr) const override; + double m_depth; + }; + + class ProjectScale : public IProjection + { + std::unique_ptr core; + double m_scale; + public: + ProjectScale(std::unique_ptr core, double scale) + : core(std::move(core)), m_scale(scale) + {} + + // Inherited via IProject + std::pair create_front_back(const Point &p) const override + { + auto res = core->create_front_back(p); + return std::make_pair(res.first * m_scale, res.second * m_scale); + } + Vec3d project(const Vec3d &point) const override{ + return core->project(point); + } + std::optional unproject(const Vec3d &p, double *depth = nullptr) const override { + auto res = core->unproject(p / m_scale, depth); + if (depth != nullptr) *depth *= m_scale; + return res; + } + }; + + class OrthoProject3d : public Emboss::IProject3d + { + // size and direction of emboss for ortho projection + Vec3d m_direction; + public: + OrthoProject3d(Vec3d direction) : m_direction(direction) {} + Vec3d project(const Vec3d &point) const override{ return point + m_direction;} + }; + + class OrthoProject: public Emboss::IProjection { + Transform3d m_matrix; + // size and direction of emboss for ortho projection + Vec3d m_direction; + Transform3d m_matrix_inv; + public: + OrthoProject(Transform3d matrix, Vec3d direction) + : m_matrix(matrix), m_direction(direction), m_matrix_inv(matrix.inverse()) + {} + // Inherited via IProject + std::pair create_front_back(const Point &p) const override; + Vec3d project(const Vec3d &point) const override; + std::optional unproject(const Vec3d &p, double * depth = nullptr) const override; + }; +} // namespace Emboss + +} // namespace Slic3r +#endif // slic3r_Emboss_hpp_ diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index 17b5b19631..98ad9e2e62 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -85,6 +85,25 @@ public: inline bool operator==(const ExPolygon &lhs, const ExPolygon &rhs) { return lhs.contour == rhs.contour && lhs.holes == rhs.holes; } inline bool operator!=(const ExPolygon &lhs, const ExPolygon &rhs) { return lhs.contour != rhs.contour || lhs.holes != rhs.holes; } +inline size_t count_points(const ExPolygons &expolys) +{ + size_t n_points = 0; + for (const auto &expoly : expolys) { + n_points += expoly.contour.points.size(); + for (const auto &hole : expoly.holes) + n_points += hole.points.size(); + } + return n_points; +} + +inline size_t count_points(const ExPolygon &expoly) +{ + size_t n_points = expoly.contour.points.size(); + for (const auto &hole : expoly.holes) + n_points += hole.points.size(); + return n_points; +} + // Count a nuber of polygons stored inside the vector of expolygons. // Useful for allocating space for polygons when converting expolygons to polygons. inline size_t number_polygons(const ExPolygons &expolys) @@ -97,11 +116,8 @@ inline size_t number_polygons(const ExPolygons &expolys) inline Lines to_lines(const ExPolygon &src) { - size_t n_lines = src.contour.points.size(); - for (size_t i = 0; i < src.holes.size(); ++ i) - n_lines += src.holes[i].points.size(); Lines lines; - lines.reserve(n_lines); + lines.reserve(count_points(src)); for (size_t i = 0; i <= src.holes.size(); ++ i) { const Polygon &poly = (i == 0) ? src.contour : src.holes[i - 1]; for (Points::const_iterator it = poly.points.begin(); it != poly.points.end()-1; ++it) @@ -113,14 +129,8 @@ inline Lines to_lines(const ExPolygon &src) inline Lines to_lines(const ExPolygons &src) { - size_t n_lines = 0; - for (ExPolygons::const_iterator it_expoly = src.begin(); it_expoly != src.end(); ++ it_expoly) { - n_lines += it_expoly->contour.points.size(); - for (size_t i = 0; i < it_expoly->holes.size(); ++ i) - n_lines += it_expoly->holes[i].points.size(); - } Lines lines; - lines.reserve(n_lines); + lines.reserve(count_points(src)); for (ExPolygons::const_iterator it_expoly = src.begin(); it_expoly != src.end(); ++ it_expoly) { for (size_t i = 0; i <= it_expoly->holes.size(); ++ i) { const Points &points = ((i == 0) ? it_expoly->contour : it_expoly->holes[i - 1]).points; @@ -132,16 +142,40 @@ inline Lines to_lines(const ExPolygons &src) return lines; } -inline std::vector to_unscaled_linesf(const ExPolygons &src) +// Line is from point index(see to_points) to next point. +// Next point of last point in polygon is first polygon point. +inline Linesf to_linesf(const ExPolygons &src, uint32_t count_lines = 0) { - size_t n_lines = 0; - for (ExPolygons::const_iterator it_expoly = src.begin(); it_expoly != src.end(); ++ it_expoly) { - n_lines += it_expoly->contour.points.size(); - for (size_t i = 0; i < it_expoly->holes.size(); ++ i) - n_lines += it_expoly->holes[i].points.size(); + assert(count_lines == 0 || count_lines == count_points(src)); + if (count_lines == 0) count_lines = count_points(src); + Linesf lines; + lines.reserve(count_lines); + Vec2d prev_pd; + auto to_lines = [&lines, &prev_pd](const Points &pts) { + assert(pts.size() >= 3); + if (pts.size() < 2) return; + bool is_first = true; + for (const Point &p : pts) { + Vec2d pd = p.cast(); + if (is_first) is_first = false; + else lines.emplace_back(prev_pd, pd); + prev_pd = pd; + } + lines.emplace_back(prev_pd, pts.front().cast()); + }; + for (const ExPolygon& expoly: src) { + to_lines(expoly.contour.points); + for (const Polygon &hole : expoly.holes) + to_lines(hole.points); } - std::vector lines; - lines.reserve(n_lines); + assert(lines.size() == count_lines); + return lines; +} + +inline Linesf to_unscaled_linesf(const ExPolygons &src) +{ + Linesf lines; + lines.reserve(count_points(src)); for (ExPolygons::const_iterator it_expoly = src.begin(); it_expoly != src.end(); ++ it_expoly) { for (size_t i = 0; i <= it_expoly->holes.size(); ++ i) { const Points &points = ((i == 0) ? it_expoly->contour : it_expoly->holes[i - 1]).points; @@ -159,6 +193,19 @@ inline std::vector to_unscaled_linesf(const ExPolygons &src) } +inline Points to_points(const ExPolygons &src) +{ + Points points; + size_t count = count_points(src); + points.reserve(count); + for (const ExPolygon &expolygon : src) { + append(points, expolygon.contour.points); + for (const Polygon &hole : expolygon.holes) + append(points, hole.points); + } + return points; +} + inline Polylines to_polylines(const ExPolygon &src) { Polylines polylines; @@ -278,8 +325,9 @@ inline Polygons to_polygons(ExPolygon &&src) Polygons polygons; polygons.reserve(src.holes.size() + 1); polygons.push_back(std::move(src.contour)); - std::move(std::begin(src.holes), std::end(src.holes), std::back_inserter(polygons)); - src.holes.clear(); + polygons.insert(polygons.end(), + std::make_move_iterator(src.holes.begin()), + std::make_move_iterator(src.holes.end())); return polygons; } @@ -287,10 +335,11 @@ inline Polygons to_polygons(ExPolygons &&src) { Polygons polygons; polygons.reserve(number_polygons(src)); - for (ExPolygons::iterator it = src.begin(); it != src.end(); ++it) { - polygons.push_back(std::move(it->contour)); - std::move(std::begin(it->holes), std::end(it->holes), std::back_inserter(polygons)); - it->holes.clear(); + for (ExPolygon& expoly: src) { + polygons.push_back(std::move(expoly.contour)); + polygons.insert(polygons.end(), + std::make_move_iterator(expoly.holes.begin()), + std::make_move_iterator(expoly.holes.end())); } return polygons; } @@ -315,11 +364,8 @@ inline ExPolygons to_expolygons(Polygons &&polys) inline Points to_points(const ExPolygon &expoly) { - size_t cnt = expoly.contour.size(); - for (const Polygon &hole : expoly.holes) - cnt += hole.size(); Points out; - out.reserve(cnt); + out.reserve(count_points(expoly)); append(out, expoly.contour.points); for (const Polygon &hole : expoly.holes) append(out, hole.points); @@ -345,18 +391,20 @@ inline void polygons_append(Polygons &dst, const ExPolygons &src) inline void polygons_append(Polygons &dst, ExPolygon &&src) { dst.reserve(dst.size() + src.holes.size() + 1); - dst.push_back(std::move(src.contour)); - std::move(std::begin(src.holes), std::end(src.holes), std::back_inserter(dst)); - src.holes.clear(); + dst.push_back(std::move(src.contour)); + dst.insert(dst.end(), + std::make_move_iterator(src.holes.begin()), + std::make_move_iterator(src.holes.end())); } inline void polygons_append(Polygons &dst, ExPolygons &&src) { dst.reserve(dst.size() + number_polygons(src)); - for (ExPolygons::iterator it = src.begin(); it != src.end(); ++ it) { - dst.push_back(std::move(it->contour)); - std::move(std::begin(it->holes), std::end(it->holes), std::back_inserter(dst)); - it->holes.clear(); + for (ExPolygon& expoly: src) { + dst.push_back(std::move(expoly.contour)); + dst.insert(dst.end(), + std::make_move_iterator(expoly.holes.begin()), + std::make_move_iterator(expoly.holes.end())); } } @@ -370,8 +418,9 @@ inline void expolygons_append(ExPolygons &dst, ExPolygons &&src) if (dst.empty()) { dst = std::move(src); } else { - std::move(std::begin(src), std::end(src), std::back_inserter(dst)); - src.clear(); + dst.insert(dst.end(), + std::make_move_iterator(src.begin()), + std::make_move_iterator(src.end())); } } diff --git a/src/libslic3r/ExPolygonsIndex.cpp b/src/libslic3r/ExPolygonsIndex.cpp new file mode 100644 index 0000000000..976531799f --- /dev/null +++ b/src/libslic3r/ExPolygonsIndex.cpp @@ -0,0 +1,82 @@ +#include "ExPolygonsIndex.hpp" +using namespace Slic3r; + +// IMPROVE: use one dimensional vector for polygons offset with searching by std::lower_bound +ExPolygonsIndices::ExPolygonsIndices(const ExPolygons &shapes) +{ + // prepare offsets + m_offsets.reserve(shapes.size()); + uint32_t offset = 0; + for (const ExPolygon &shape : shapes) { + assert(!shape.contour.points.empty()); + std::vector shape_offsets; + shape_offsets.reserve(shape.holes.size() + 1); + shape_offsets.push_back(offset); + offset += shape.contour.points.size(); + for (const Polygon &hole: shape.holes) { + shape_offsets.push_back(offset); + offset += hole.points.size(); + } + m_offsets.push_back(std::move(shape_offsets)); + } + m_count = offset; +} + +uint32_t ExPolygonsIndices::cvt(const ExPolygonsIndex &id) const +{ + assert(id.expolygons_index < m_offsets.size()); + const std::vector &shape_offset = m_offsets[id.expolygons_index]; + assert(id.polygon_index < shape_offset.size()); + uint32_t res = shape_offset[id.polygon_index] + id.point_index; + assert(res < m_count); + return res; +} + +ExPolygonsIndex ExPolygonsIndices::cvt(uint32_t index) const +{ + assert(index < m_count); + ExPolygonsIndex result{0, 0, 0}; + // find expolygon index + auto fn = [](const std::vector &offsets, uint32_t index) { return offsets[0] < index; }; + auto it = std::lower_bound(m_offsets.begin() + 1, m_offsets.end(), index, fn); + result.expolygons_index = it - m_offsets.begin(); + if (it == m_offsets.end() || it->at(0) != index) --result.expolygons_index; + + // find polygon index + const std::vector &shape_offset = m_offsets[result.expolygons_index]; + auto it2 = std::lower_bound(shape_offset.begin() + 1, shape_offset.end(), index); + result.polygon_index = it2 - shape_offset.begin(); + if (it2 == shape_offset.end() || *it2 != index) --result.polygon_index; + + // calculate point index + uint32_t polygon_offset = shape_offset[result.polygon_index]; + assert(index >= polygon_offset); + result.point_index = index - polygon_offset; + return result; +} + +bool ExPolygonsIndices::is_last_point(const ExPolygonsIndex &id) const { + assert(id.expolygons_index < m_offsets.size()); + const std::vector &shape_offset = m_offsets[id.expolygons_index]; + assert(id.polygon_index < shape_offset.size()); + uint32_t index = shape_offset[id.polygon_index] + id.point_index; + assert(index < m_count); + // next index + uint32_t next_point_index = index + 1; + uint32_t next_poly_index = id.polygon_index + 1; + uint32_t next_expoly_index = id.expolygons_index + 1; + // is last expoly? + if (next_expoly_index == m_offsets.size()) { + // is last expoly last poly? + if (next_poly_index == shape_offset.size()) + return next_point_index == m_count; + } else { + // (not last expoly) is expoly last poly? + if (next_poly_index == shape_offset.size()) + return next_point_index == m_offsets[next_expoly_index][0]; + } + // Not last polygon in expolygon + return next_point_index == shape_offset[next_poly_index]; +} + +uint32_t ExPolygonsIndices::get_count() const { return m_count; } diff --git a/src/libslic3r/ExPolygonsIndex.hpp b/src/libslic3r/ExPolygonsIndex.hpp new file mode 100644 index 0000000000..b46fd50890 --- /dev/null +++ b/src/libslic3r/ExPolygonsIndex.hpp @@ -0,0 +1,74 @@ +#ifndef slic3r_ExPolygonsIndex_hpp_ +#define slic3r_ExPolygonsIndex_hpp_ + +#include "ExPolygon.hpp" +namespace Slic3r { + +/// +/// Index into ExPolygons +/// Identify expolygon, its contour (or hole) and point +/// +struct ExPolygonsIndex +{ + // index of ExPolygons + uint32_t expolygons_index; + + // index of Polygon + // 0 .. contour + // N .. hole[N-1] + uint32_t polygon_index; + + // index of point in polygon + uint32_t point_index; + + bool is_contour() const { return polygon_index == 0; } + bool is_hole() const { return polygon_index != 0; } + uint32_t hole_index() const { return polygon_index - 1; } +}; + +/// +/// Keep conversion from ExPolygonsIndex to Index and vice versa +/// ExPolygonsIndex .. contour(or hole) point from ExPolygons +/// Index .. continous number +/// +/// index is used to address lines and points as result from function +/// Slic3r::to_lines, Slic3r::to_points +/// +class ExPolygonsIndices +{ + std::vector> m_offsets; + // for check range of index + uint32_t m_count; // count of points +public: + ExPolygonsIndices(const ExPolygons &shapes); + + /// + /// Convert to one index number + /// + /// Compose of adress into expolygons + /// Index + uint32_t cvt(const ExPolygonsIndex &id) const; + + /// + /// Separate to multi index + /// + /// adress into expolygons + /// + ExPolygonsIndex cvt(uint32_t index) const; + + /// + /// Check whether id is last point in polygon + /// + /// Identify point in expolygon + /// True when id is last point in polygon otherwise false + bool is_last_point(const ExPolygonsIndex &id) const; + + /// + /// Count of points in expolygons + /// + /// Count of points in expolygons + uint32_t get_count() const; +}; + +} // namespace Slic3r +#endif // slic3r_ExPolygonsIndex_hpp_ diff --git a/src/libslic3r/ExtrusionEntityCollection.hpp b/src/libslic3r/ExtrusionEntityCollection.hpp index 59d4421d69..cabf4e2f93 100644 --- a/src/libslic3r/ExtrusionEntityCollection.hpp +++ b/src/libslic3r/ExtrusionEntityCollection.hpp @@ -76,7 +76,11 @@ public: if (entities.empty()) entities = std::move(src); else { - std::move(std::begin(src), std::end(src), std::back_inserter(entities)); + entities.insert(entities.end(), + std::make_move_iterator(src.begin()), + std::make_move_iterator(src.end())); + // Removing pointers to polymorphic extrusions from the donor object + // so that they will not be deleted twice. src.clear(); } } diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index f2056b8bd6..bc379814e9 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -1,3403 +1,3669 @@ -#include "../libslic3r.h" -#include "../Exception.hpp" -#include "../Model.hpp" -#include "../Utils.hpp" -#include "../LocalesUtils.hpp" -#include "../GCode.hpp" -#include "../Geometry.hpp" -#include "../GCode/ThumbnailData.hpp" -#include "../Semver.hpp" -#include "../Time.hpp" - -#include "../I18N.hpp" - -#include "3mf.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -namespace pt = boost::property_tree; - -#include -#include -#include "miniz_extension.hpp" - -#include - -// Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, -// https://github.com/boostorg/spirit/pull/586 -// where the exported string is one digit shorter than it should be to guarantee lossless round trip. -// The code is left here for the ocasion boost guys improve. -#define EXPORT_3MF_USE_SPIRIT_KARMA_FP 0 - -// VERSION NUMBERS -// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them. -// 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files. -// 2 : Volumes' matrices and source data added to Metadata/Slic3r_PE_model.config file, meshes transformed back to their coordinate system on loading. -// WARNING !! -> the version number has been rolled back to 1 -// the next change should use 3 -const unsigned int VERSION_3MF = 1; -// Allow loading version 2 file as well. -const unsigned int VERSION_3MF_COMPATIBLE = 2; -const char* SLIC3RPE_3MF_VERSION = "slic3rpe:Version3mf"; // definition of the metadata name saved into .model file - -// Painting gizmos data version numbers -// 0 : 3MF files saved by older PrusaSlicer or the painting gizmo wasn't used. No version definition in them. -// 1 : Introduction of painting gizmos data versioning. No other changes in painting gizmos data. -const unsigned int FDM_SUPPORTS_PAINTING_VERSION = 1; -const unsigned int SEAM_PAINTING_VERSION = 1; -const unsigned int MM_PAINTING_VERSION = 1; - -const std::string SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION = "slic3rpe:FdmSupportsPaintingVersion"; -const std::string SLIC3RPE_SEAM_PAINTING_VERSION = "slic3rpe:SeamPaintingVersion"; -const std::string SLIC3RPE_MM_PAINTING_VERSION = "slic3rpe:MmPaintingVersion"; - -const std::string MODEL_FOLDER = "3D/"; -const std::string MODEL_EXTENSION = ".model"; -const std::string MODEL_FILE = "3D/3dmodel.model"; // << this is the only format of the string which works with CURA -const std::string CONTENT_TYPES_FILE = "[Content_Types].xml"; -const std::string RELATIONSHIPS_FILE = "_rels/.rels"; -const std::string THUMBNAIL_FILE = "Metadata/thumbnail.png"; -const std::string PRINT_CONFIG_FILE = "Metadata/Slic3r_PE.config"; -const std::string MODEL_CONFIG_FILE = "Metadata/Slic3r_PE_model.config"; -const std::string LAYER_HEIGHTS_PROFILE_FILE = "Metadata/Slic3r_PE_layer_heights_profile.txt"; -const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/Prusa_Slicer_layer_config_ranges.xml"; -const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt"; -const std::string SLA_DRAIN_HOLES_FILE = "Metadata/Slic3r_PE_sla_drain_holes.txt"; -const std::string CUSTOM_GCODE_PER_PRINT_Z_FILE = "Metadata/Prusa_Slicer_custom_gcode_per_print_z.xml"; -const std::string CUT_INFORMATION_FILE = "Metadata/Prusa_Slicer_cut_information.xml"; - -static constexpr const char* MODEL_TAG = "model"; -static constexpr const char* RESOURCES_TAG = "resources"; -static constexpr const char* OBJECT_TAG = "object"; -static constexpr const char* MESH_TAG = "mesh"; -static constexpr const char* VERTICES_TAG = "vertices"; -static constexpr const char* VERTEX_TAG = "vertex"; -static constexpr const char* TRIANGLES_TAG = "triangles"; -static constexpr const char* TRIANGLE_TAG = "triangle"; -static constexpr const char* COMPONENTS_TAG = "components"; -static constexpr const char* COMPONENT_TAG = "component"; -static constexpr const char* BUILD_TAG = "build"; -static constexpr const char* ITEM_TAG = "item"; -static constexpr const char* METADATA_TAG = "metadata"; - -static constexpr const char* CONFIG_TAG = "config"; -static constexpr const char* VOLUME_TAG = "volume"; - -static constexpr const char* UNIT_ATTR = "unit"; -static constexpr const char* NAME_ATTR = "name"; -static constexpr const char* TYPE_ATTR = "type"; -static constexpr const char* ID_ATTR = "id"; -static constexpr const char* X_ATTR = "x"; -static constexpr const char* Y_ATTR = "y"; -static constexpr const char* Z_ATTR = "z"; -static constexpr const char* V1_ATTR = "v1"; -static constexpr const char* V2_ATTR = "v2"; -static constexpr const char* V3_ATTR = "v3"; -static constexpr const char* OBJECTID_ATTR = "objectid"; -static constexpr const char* TRANSFORM_ATTR = "transform"; -static constexpr const char* PRINTABLE_ATTR = "printable"; -static constexpr const char* INSTANCESCOUNT_ATTR = "instances_count"; -static constexpr const char* CUSTOM_SUPPORTS_ATTR = "slic3rpe:custom_supports"; -static constexpr const char* CUSTOM_SEAM_ATTR = "slic3rpe:custom_seam"; -static constexpr const char* MMU_SEGMENTATION_ATTR = "slic3rpe:mmu_segmentation"; - -static constexpr const char* KEY_ATTR = "key"; -static constexpr const char* VALUE_ATTR = "value"; -static constexpr const char* FIRST_TRIANGLE_ID_ATTR = "firstid"; -static constexpr const char* LAST_TRIANGLE_ID_ATTR = "lastid"; - -static constexpr const char* OBJECT_TYPE = "object"; -static constexpr const char* VOLUME_TYPE = "volume"; - -static constexpr const char* NAME_KEY = "name"; -static constexpr const char* MODIFIER_KEY = "modifier"; -static constexpr const char* VOLUME_TYPE_KEY = "volume_type"; -static constexpr const char* MATRIX_KEY = "matrix"; -static constexpr const char* SOURCE_FILE_KEY = "source_file"; -static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id"; -static constexpr const char* SOURCE_VOLUME_ID_KEY = "source_volume_id"; -static constexpr const char* SOURCE_OFFSET_X_KEY = "source_offset_x"; -static constexpr const char* SOURCE_OFFSET_Y_KEY = "source_offset_y"; -static constexpr const char* SOURCE_OFFSET_Z_KEY = "source_offset_z"; -static constexpr const char* SOURCE_IN_INCHES_KEY = "source_in_inches"; -static constexpr const char* SOURCE_IN_METERS_KEY = "source_in_meters"; -#if ENABLE_RELOAD_FROM_DISK_REWORK -static constexpr const char* SOURCE_IS_BUILTIN_VOLUME_KEY = "source_is_builtin_volume"; -#endif // ENABLE_RELOAD_FROM_DISK_REWORK - -static constexpr const char* MESH_STAT_EDGES_FIXED = "edges_fixed"; -static constexpr const char* MESH_STAT_DEGENERATED_FACETS = "degenerate_facets"; -static constexpr const char* MESH_STAT_FACETS_REMOVED = "facets_removed"; -static constexpr const char* MESH_STAT_FACETS_RESERVED = "facets_reversed"; -static constexpr const char* MESH_STAT_BACKWARDS_EDGES = "backwards_edges"; - - -const unsigned int VALID_OBJECT_TYPES_COUNT = 1; -const char* VALID_OBJECT_TYPES[] = -{ - "model" -}; - -const char* INVALID_OBJECT_TYPES[] = -{ - "solidsupport", - "support", - "surface", - "other" -}; - -class version_error : public Slic3r::FileIOError -{ -public: - version_error(const std::string& what_arg) : Slic3r::FileIOError(what_arg) {} - version_error(const char* what_arg) : Slic3r::FileIOError(what_arg) {} -}; - -const char* get_attribute_value_charptr(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - if ((attributes == nullptr) || (attributes_size == 0) || (attributes_size % 2 != 0) || (attribute_key == nullptr)) - return nullptr; - - for (unsigned int a = 0; a < attributes_size; a += 2) { - if (::strcmp(attributes[a], attribute_key) == 0) - return attributes[a + 1]; - } - - return nullptr; -} - -std::string get_attribute_value_string(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - const char* text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); - return (text != nullptr) ? text : ""; -} - -float get_attribute_value_float(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - float value = 0.0f; - if (const char *text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); text != nullptr) - fast_float::from_chars(text, text + strlen(text), value); - return value; -} - -int get_attribute_value_int(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - int value = 0; - if (const char *text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); text != nullptr) - boost::spirit::qi::parse(text, text + strlen(text), boost::spirit::qi::int_, value); - return value; -} - -bool get_attribute_value_bool(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - const char* text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); - return (text != nullptr) ? (bool)::atoi(text) : true; -} - -Slic3r::Transform3d get_transform_from_3mf_specs_string(const std::string& mat_str) -{ - // check: https://3mf.io/3d-manufacturing-format/ or https://github.com/3MFConsortium/spec_core/blob/master/3MF%20Core%20Specification.md - // to see how matrices are stored inside 3mf according to specifications - Slic3r::Transform3d ret = Slic3r::Transform3d::Identity(); - - if (mat_str.empty()) - // empty string means default identity matrix - return ret; - - std::vector mat_elements_str; - boost::split(mat_elements_str, mat_str, boost::is_any_of(" "), boost::token_compress_on); - - unsigned int size = (unsigned int)mat_elements_str.size(); - if (size != 12) - // invalid data, return identity matrix - return ret; - - unsigned int i = 0; - // matrices are stored into 3mf files as 4x3 - // we need to transpose them - for (unsigned int c = 0; c < 4; ++c) { - for (unsigned int r = 0; r < 3; ++r) { - ret(r, c) = ::atof(mat_elements_str[i++].c_str()); - } - } - return ret; -} - -float get_unit_factor(const std::string& unit) -{ - const char* text = unit.c_str(); - - if (::strcmp(text, "micron") == 0) - return 0.001f; - else if (::strcmp(text, "centimeter") == 0) - return 10.0f; - else if (::strcmp(text, "inch") == 0) - return 25.4f; - else if (::strcmp(text, "foot") == 0) - return 304.8f; - else if (::strcmp(text, "meter") == 0) - return 1000.0f; - else - // default "millimeters" (see specification) - return 1.0f; -} - -bool is_valid_object_type(const std::string& type) -{ - // if the type is empty defaults to "model" (see specification) - if (type.empty()) - return true; - - for (unsigned int i = 0; i < VALID_OBJECT_TYPES_COUNT; ++i) { - if (::strcmp(type.c_str(), VALID_OBJECT_TYPES[i]) == 0) - return true; - } - - return false; -} - -namespace Slic3r { - -//! macro used to mark string used at localization, -//! return same string -#define L(s) (s) -#define _(s) Slic3r::I18N::translate(s) - - // Base class with error messages management - class _3MF_Base - { - std::vector m_errors; - - protected: - void add_error(const std::string& error) { m_errors.push_back(error); } - void clear_errors() { m_errors.clear(); } - - public: - void log_errors() - { - for (const std::string& error : m_errors) - BOOST_LOG_TRIVIAL(error) << error; - } - }; - - class _3MF_Importer : public _3MF_Base - { - struct Component - { - int object_id; - Transform3d transform; - - explicit Component(int object_id) - : object_id(object_id) - , transform(Transform3d::Identity()) - { - } - - Component(int object_id, const Transform3d& transform) - : object_id(object_id) - , transform(transform) - { - } - }; - - typedef std::vector ComponentsList; - - struct Geometry - { - std::vector vertices; - std::vector triangles; - std::vector custom_supports; - std::vector custom_seam; - std::vector mmu_segmentation; - - bool empty() { return vertices.empty() || triangles.empty(); } - - void reset() { - vertices.clear(); - triangles.clear(); - custom_supports.clear(); - custom_seam.clear(); - mmu_segmentation.clear(); - } - }; - - struct CurrentObject - { - // ID of the object inside the 3MF file, 1 based. - int id; - // Index of the ModelObject in its respective Model, zero based. - int model_object_idx; - Geometry geometry; - ModelObject* object; - ComponentsList components; - - CurrentObject() { reset(); } - - void reset() { - id = -1; - model_object_idx = -1; - geometry.reset(); - object = nullptr; - components.clear(); - } - }; - - struct CurrentConfig - { - int object_id; - int volume_id; - }; - - struct Instance - { - ModelInstance* instance; - Transform3d transform; - - Instance(ModelInstance* instance, const Transform3d& transform) - : instance(instance) - , transform(transform) - { - } - }; - - struct Metadata - { - std::string key; - std::string value; - - Metadata(const std::string& key, const std::string& value) - : key(key) - , value(value) - { - } - }; - - typedef std::vector MetadataList; - - struct ObjectMetadata - { - struct VolumeMetadata - { - unsigned int first_triangle_id; - unsigned int last_triangle_id; - MetadataList metadata; - RepairedMeshErrors mesh_stats; - - VolumeMetadata(unsigned int first_triangle_id, unsigned int last_triangle_id) - : first_triangle_id(first_triangle_id) - , last_triangle_id(last_triangle_id) - { - } - }; - - typedef std::vector VolumeMetadataList; - - MetadataList metadata; - VolumeMetadataList volumes; - }; - - struct CutObjectInfo - { - struct Connector - { - int volume_id; - int type; - float r_tolerance; - float h_tolerance; - }; - CutObjectBase id; - std::vector connectors; - }; - - // Map from a 1 based 3MF object ID to a 0 based ModelObject index inside m_model->objects. - typedef std::map IdToModelObjectMap; - typedef std::map IdToAliasesMap; - typedef std::vector InstancesList; - typedef std::map IdToMetadataMap; - typedef std::map IdToGeometryMap; - typedef std::map> IdToLayerHeightsProfileMap; - typedef std::map IdToLayerConfigRangesMap; - typedef std::map IdToCutObjectInfoMap; - typedef std::map> IdToSlaSupportPointsMap; - typedef std::map> IdToSlaDrainHolesMap; - - // Version of the 3mf file - unsigned int m_version; - bool m_check_version; - - // Semantic version of PrusaSlicer, that generated this 3MF. - boost::optional m_prusaslicer_generator_version; - unsigned int m_fdm_supports_painting_version = 0; - unsigned int m_seam_painting_version = 0; - unsigned int m_mm_painting_version = 0; - - XML_Parser m_xml_parser; - // Error code returned by the application side of the parser. In that case the expat may not reliably deliver the error state - // after returning from XML_Parse() function, thus we keep the error state here. - bool m_parse_error { false }; - std::string m_parse_error_message; - Model* m_model; - float m_unit_factor; - CurrentObject m_curr_object; - IdToModelObjectMap m_objects; - IdToAliasesMap m_objects_aliases; - InstancesList m_instances; - IdToGeometryMap m_geometries; - CurrentConfig m_curr_config; - IdToMetadataMap m_objects_metadata; - IdToCutObjectInfoMap m_cut_object_infos; - IdToLayerHeightsProfileMap m_layer_heights_profiles; - IdToLayerConfigRangesMap m_layer_config_ranges; - IdToSlaSupportPointsMap m_sla_support_points; - IdToSlaDrainHolesMap m_sla_drain_holes; - std::string m_curr_metadata_name; - std::string m_curr_characters; - std::string m_name; - - public: - _3MF_Importer(); - ~_3MF_Importer(); - - bool load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version); - unsigned int version() const { return m_version; } - boost::optional prusaslicer_generator_version() const { return m_prusaslicer_generator_version; } - - private: - void _destroy_xml_parser(); - void _stop_xml_parser(const std::string& msg = std::string()); - - bool parse_error() const { return m_parse_error; } - const char* parse_error_message() const { - return m_parse_error ? - // The error was signalled by the user code, not the expat parser. - (m_parse_error_message.empty() ? "Invalid 3MF format" : m_parse_error_message.c_str()) : - // The error was signalled by the expat parser. - XML_ErrorString(XML_GetErrorCode(m_xml_parser)); - } - - bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); - bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - void _extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); - void _extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - void _extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); - void _extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - void _extract_sla_drain_holes_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - - void _extract_custom_gcode_per_print_z_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - - void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& subs_context, const std::string& archive_filename); - bool _extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); - - // handlers to parse the .model file - void _handle_start_model_xml_element(const char* name, const char** attributes); - void _handle_end_model_xml_element(const char* name); - void _handle_model_xml_characters(const XML_Char* s, int len); - - // handlers to parse the MODEL_CONFIG_FILE file - void _handle_start_config_xml_element(const char* name, const char** attributes); - void _handle_end_config_xml_element(const char* name); - - bool _handle_start_model(const char** attributes, unsigned int num_attributes); - bool _handle_end_model(); - - bool _handle_start_resources(const char** attributes, unsigned int num_attributes); - bool _handle_end_resources(); - - bool _handle_start_object(const char** attributes, unsigned int num_attributes); - bool _handle_end_object(); - - bool _handle_start_mesh(const char** attributes, unsigned int num_attributes); - bool _handle_end_mesh(); - - bool _handle_start_vertices(const char** attributes, unsigned int num_attributes); - bool _handle_end_vertices(); - - bool _handle_start_vertex(const char** attributes, unsigned int num_attributes); - bool _handle_end_vertex(); - - bool _handle_start_triangles(const char** attributes, unsigned int num_attributes); - bool _handle_end_triangles(); - - bool _handle_start_triangle(const char** attributes, unsigned int num_attributes); - bool _handle_end_triangle(); - - bool _handle_start_components(const char** attributes, unsigned int num_attributes); - bool _handle_end_components(); - - bool _handle_start_component(const char** attributes, unsigned int num_attributes); - bool _handle_end_component(); - - bool _handle_start_build(const char** attributes, unsigned int num_attributes); - bool _handle_end_build(); - - bool _handle_start_item(const char** attributes, unsigned int num_attributes); - bool _handle_end_item(); - - bool _handle_start_metadata(const char** attributes, unsigned int num_attributes); - bool _handle_end_metadata(); - - bool _create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter); - - void _apply_transform(ModelInstance& instance, const Transform3d& transform); - - bool _handle_start_config(const char** attributes, unsigned int num_attributes); - bool _handle_end_config(); - - bool _handle_start_config_object(const char** attributes, unsigned int num_attributes); - bool _handle_end_config_object(); - - bool _handle_start_config_volume(const char** attributes, unsigned int num_attributes); - bool _handle_start_config_volume_mesh(const char** attributes, unsigned int num_attributes); - bool _handle_end_config_volume(); - bool _handle_end_config_volume_mesh(); - - bool _handle_start_config_metadata(const char** attributes, unsigned int num_attributes); - bool _handle_end_config_metadata(); - - bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); - - // callbacks to parse the .model file - static void XMLCALL _handle_start_model_xml_element(void* userData, const char* name, const char** attributes); - static void XMLCALL _handle_end_model_xml_element(void* userData, const char* name); - static void XMLCALL _handle_model_xml_characters(void* userData, const XML_Char* s, int len); - - // callbacks to parse the MODEL_CONFIG_FILE file - static void XMLCALL _handle_start_config_xml_element(void* userData, const char* name, const char** attributes); - static void XMLCALL _handle_end_config_xml_element(void* userData, const char* name); - }; - - _3MF_Importer::_3MF_Importer() - : m_version(0) - , m_check_version(false) - , m_xml_parser(nullptr) - , m_model(nullptr) - , m_unit_factor(1.0f) - , m_curr_metadata_name("") - , m_curr_characters("") - , m_name("") - { - } - - _3MF_Importer::~_3MF_Importer() - { - _destroy_xml_parser(); - } - - bool _3MF_Importer::load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version) - { - m_version = 0; - m_fdm_supports_painting_version = 0; - m_seam_painting_version = 0; - m_mm_painting_version = 0; - m_check_version = check_version; - m_model = &model; - m_unit_factor = 1.0f; - m_curr_object.reset(); - m_objects.clear(); - m_objects_aliases.clear(); - m_instances.clear(); - m_geometries.clear(); - m_curr_config.object_id = -1; - m_curr_config.volume_id = -1; - m_objects_metadata.clear(); - m_layer_heights_profiles.clear(); - m_layer_config_ranges.clear(); - m_sla_support_points.clear(); - m_curr_metadata_name.clear(); - m_curr_characters.clear(); - clear_errors(); - - return _load_model_from_file(filename, model, config, config_substitutions); - } - - void _3MF_Importer::_destroy_xml_parser() - { - if (m_xml_parser != nullptr) { - XML_ParserFree(m_xml_parser); - m_xml_parser = nullptr; - } - } - - void _3MF_Importer::_stop_xml_parser(const std::string &msg) - { - assert(! m_parse_error); - assert(m_parse_error_message.empty()); - assert(m_xml_parser != nullptr); - m_parse_error = true; - m_parse_error_message = msg; - XML_StopParser(m_xml_parser, false); - } - - bool _3MF_Importer::_load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions) - { - mz_zip_archive archive; - mz_zip_zero_struct(&archive); - - if (!open_zip_reader(&archive, filename)) { - add_error("Unable to open the file"); - return false; - } - - mz_uint num_entries = mz_zip_reader_get_num_files(&archive); - - mz_zip_archive_file_stat stat; - - m_name = boost::filesystem::path(filename).stem().string(); - - // we first loop the entries to read from the archive the .model file only, in order to extract the version from it - for (mz_uint i = 0; i < num_entries; ++i) { - if (mz_zip_reader_file_stat(&archive, i, &stat)) { - std::string name(stat.m_filename); - std::replace(name.begin(), name.end(), '\\', '/'); - - if (boost::algorithm::istarts_with(name, MODEL_FOLDER) && boost::algorithm::iends_with(name, MODEL_EXTENSION)) { - try - { - // valid model name -> extract model - if (!_extract_model_from_archive(archive, stat)) { - close_zip_reader(&archive); - add_error("Archive does not contain a valid model"); - return false; - } - } - catch (const std::exception& e) - { - // ensure the zip archive is closed and rethrow the exception - close_zip_reader(&archive); - throw Slic3r::FileIOError(e.what()); - } - } - } - } - - // we then loop again the entries to read other files stored in the archive - for (mz_uint i = 0; i < num_entries; ++i) { - if (mz_zip_reader_file_stat(&archive, i, &stat)) { - std::string name(stat.m_filename); - std::replace(name.begin(), name.end(), '\\', '/'); - - if (boost::algorithm::iequals(name, LAYER_HEIGHTS_PROFILE_FILE)) { - // extract slic3r layer heights profile file - _extract_layer_heights_profile_config_from_archive(archive, stat); - } - else if (boost::algorithm::iequals(name, CUT_INFORMATION_FILE)) { - // extract slic3r layer config ranges file - _extract_cut_information_from_archive(archive, stat, config_substitutions); - } - else if (boost::algorithm::iequals(name, LAYER_CONFIG_RANGES_FILE)) { - // extract slic3r layer config ranges file - _extract_layer_config_ranges_from_archive(archive, stat, config_substitutions); - } - else if (boost::algorithm::iequals(name, SLA_SUPPORT_POINTS_FILE)) { - // extract sla support points file - _extract_sla_support_points_from_archive(archive, stat); - } - else if (boost::algorithm::iequals(name, SLA_DRAIN_HOLES_FILE)) { - // extract sla support points file - _extract_sla_drain_holes_from_archive(archive, stat); - } - else if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { - // extract slic3r print config file - _extract_print_config_from_archive(archive, stat, config, config_substitutions, filename); - } - else if (boost::algorithm::iequals(name, CUSTOM_GCODE_PER_PRINT_Z_FILE)) { - // extract slic3r layer config ranges file - _extract_custom_gcode_per_print_z_from_archive(archive, stat); - } - else if (boost::algorithm::iequals(name, MODEL_CONFIG_FILE)) { - // extract slic3r model config file - if (!_extract_model_config_from_archive(archive, stat, model)) { - close_zip_reader(&archive); - add_error("Archive does not contain a valid model config"); - return false; - } - } - } - } - - close_zip_reader(&archive); - - if (m_version == 0) { - // if the 3mf was not produced by PrusaSlicer and there is more than one instance, - // split the object in as many objects as instances - size_t curr_models_count = m_model->objects.size(); - size_t i = 0; - while (i < curr_models_count) { - ModelObject* model_object = m_model->objects[i]; - if (model_object->instances.size() > 1) { - // select the geometry associated with the original model object - const Geometry* geometry = nullptr; - for (const IdToModelObjectMap::value_type& object : m_objects) { - if (object.second == int(i)) { - IdToGeometryMap::const_iterator obj_geometry = m_geometries.find(object.first); - if (obj_geometry == m_geometries.end()) { - add_error("Unable to find object geometry"); - return false; - } - geometry = &obj_geometry->second; - break; - } - } - - if (geometry == nullptr) { - add_error("Unable to find object geometry"); - return false; - } - - // use the geometry to create the volumes in the new model objects - ObjectMetadata::VolumeMetadataList volumes(1, { 0, (unsigned int)geometry->triangles.size() - 1 }); - - // for each instance after the 1st, create a new model object containing only that instance - // and copy into it the geometry - while (model_object->instances.size() > 1) { - ModelObject* new_model_object = m_model->add_object(*model_object); - new_model_object->clear_instances(); - new_model_object->add_instance(*model_object->instances.back()); - model_object->delete_last_instance(); - if (!_generate_volumes(*new_model_object, *geometry, volumes, config_substitutions)) - return false; - } - } - ++i; - } - } - - for (const IdToModelObjectMap::value_type& object : m_objects) { - if (object.second >= int(m_model->objects.size())) { - add_error("Unable to find object"); - return false; - } - ModelObject* model_object = m_model->objects[object.second]; - IdToGeometryMap::const_iterator obj_geometry = m_geometries.find(object.first); - if (obj_geometry == m_geometries.end()) { - add_error("Unable to find object geometry"); - return false; - } - - // m_layer_heights_profiles are indexed by a 1 based model object index. - IdToLayerHeightsProfileMap::iterator obj_layer_heights_profile = m_layer_heights_profiles.find(object.second + 1); - if (obj_layer_heights_profile != m_layer_heights_profiles.end()) - model_object->layer_height_profile.set(std::move(obj_layer_heights_profile->second)); - - // m_layer_config_ranges are indexed by a 1 based model object index. - IdToLayerConfigRangesMap::iterator obj_layer_config_ranges = m_layer_config_ranges.find(object.second + 1); - if (obj_layer_config_ranges != m_layer_config_ranges.end()) - model_object->layer_config_ranges = std::move(obj_layer_config_ranges->second); - - // m_sla_support_points are indexed by a 1 based model object index. - IdToSlaSupportPointsMap::iterator obj_sla_support_points = m_sla_support_points.find(object.second + 1); - if (obj_sla_support_points != m_sla_support_points.end() && !obj_sla_support_points->second.empty()) { - model_object->sla_support_points = std::move(obj_sla_support_points->second); - model_object->sla_points_status = sla::PointsStatus::UserModified; - } - - IdToSlaDrainHolesMap::iterator obj_drain_holes = m_sla_drain_holes.find(object.second + 1); - if (obj_drain_holes != m_sla_drain_holes.end() && !obj_drain_holes->second.empty()) { - model_object->sla_drain_holes = std::move(obj_drain_holes->second); - } - - ObjectMetadata::VolumeMetadataList volumes; - ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr; - - IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first); - if (obj_metadata != m_objects_metadata.end()) { - // config data has been found, this model was saved using slic3r pe - - // apply object's name and config data - for (const Metadata& metadata : obj_metadata->second.metadata) { - if (metadata.key == "name") - model_object->name = metadata.value; - else - model_object->config.set_deserialize(metadata.key, metadata.value, config_substitutions); - } - - // select object's detected volumes - volumes_ptr = &obj_metadata->second.volumes; - } - else { - // config data not found, this model was not saved using slic3r pe - - // add the entire geometry as the single volume to generate - volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1); - - // select as volumes - volumes_ptr = &volumes; - } - - if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr, config_substitutions)) - return false; - - // Apply cut information for object if any was loaded - // m_cut_object_ids are indexed by a 1 based model object index. - IdToCutObjectInfoMap::iterator cut_object_info = m_cut_object_infos.find(object.second + 1); - if (cut_object_info != m_cut_object_infos.end()) { - model_object->cut_id = cut_object_info->second.id; - - for (auto connector : cut_object_info->second.connectors) { - assert(0 <= connector.volume_id && connector.volume_id <= int(model_object->volumes.size())); - model_object->volumes[connector.volume_id]->cut_info = - ModelVolume::CutInfo(CutConnectorType(connector.type), connector.r_tolerance, connector.h_tolerance, true); - } - } - } - - // If instances contain a single volume, the volume offset should be 0,0,0 - // This equals to say that instance world position and volume world position should match - // Correct all instances/volumes for which this does not hold - for (int obj_id = 0; obj_id < int(model.objects.size()); ++obj_id) { - ModelObject* o = model.objects[obj_id]; - if (o->volumes.size() == 1) { - ModelVolume* v = o->volumes.front(); - const Slic3r::Geometry::Transformation& first_inst_trafo = o->instances.front()->get_transformation(); - const Vec3d world_vol_offset = (first_inst_trafo * v->get_transformation()).get_offset(); - const Vec3d world_inst_offset = first_inst_trafo.get_offset(); - - if (!world_vol_offset.isApprox(world_inst_offset)) { - const Slic3r::Geometry::Transformation& vol_trafo = v->get_transformation(); - for (int inst_id = 0; inst_id < int(o->instances.size()); ++inst_id) { - ModelInstance* i = o->instances[inst_id]; - const Slic3r::Geometry::Transformation& inst_trafo = i->get_transformation(); - i->set_offset((inst_trafo * vol_trafo).get_offset()); - } - v->set_offset(Vec3d::Zero()); - } - } - } - -#if ENABLE_RELOAD_FROM_DISK_REWORK - for (int obj_id = 0; obj_id < int(model.objects.size()); ++obj_id) { - ModelObject* o = model.objects[obj_id]; - for (int vol_id = 0; vol_id < int(o->volumes.size()); ++vol_id) { - ModelVolume* v = o->volumes[vol_id]; - if (v->source.input_file.empty()) - v->source.input_file = v->name.empty() ? filename : v->name; - if (v->source.volume_idx == -1) - v->source.volume_idx = vol_id; - if (v->source.object_idx == -1) - v->source.object_idx = obj_id; - } - } -#else - int object_idx = 0; - for (ModelObject* o : model.objects) { - int volume_idx = 0; - for (ModelVolume* v : o->volumes) { - if (v->source.input_file.empty() && v->type() == ModelVolumeType::MODEL_PART) { - v->source.input_file = filename; - if (v->source.volume_idx == -1) - v->source.volume_idx = volume_idx; - if (v->source.object_idx == -1) - v->source.object_idx = object_idx; - } - ++volume_idx; - } - ++object_idx; - } -#endif // ENABLE_RELOAD_FROM_DISK_REWORK - -// // fixes the min z of the model if negative -// model.adjust_min_z(); - - return true; - } - - bool _3MF_Importer::_extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) - { - if (stat.m_uncomp_size == 0) { - add_error("Found invalid size"); - return false; - } - - _destroy_xml_parser(); - - m_xml_parser = XML_ParserCreate(nullptr); - if (m_xml_parser == nullptr) { - add_error("Unable to create parser"); - return false; - } - - XML_SetUserData(m_xml_parser, (void*)this); - XML_SetElementHandler(m_xml_parser, _3MF_Importer::_handle_start_model_xml_element, _3MF_Importer::_handle_end_model_xml_element); - XML_SetCharacterDataHandler(m_xml_parser, _3MF_Importer::_handle_model_xml_characters); - - struct CallbackData - { - XML_Parser& parser; - _3MF_Importer& importer; - const mz_zip_archive_file_stat& stat; - - CallbackData(XML_Parser& parser, _3MF_Importer& importer, const mz_zip_archive_file_stat& stat) : parser(parser), importer(importer), stat(stat) {} - }; - - CallbackData data(m_xml_parser, *this, stat); - - mz_bool res = 0; - - try - { - res = mz_zip_reader_extract_file_to_callback(&archive, stat.m_filename, [](void* pOpaque, mz_uint64 file_ofs, const void* pBuf, size_t n)->size_t { - CallbackData* data = (CallbackData*)pOpaque; - if (!XML_Parse(data->parser, (const char*)pBuf, (int)n, (file_ofs + n == data->stat.m_uncomp_size) ? 1 : 0) || data->importer.parse_error()) { - char error_buf[1024]; - ::sprintf(error_buf, "Error (%s) while parsing '%s' at line %d", data->importer.parse_error_message(), data->stat.m_filename, (int)XML_GetCurrentLineNumber(data->parser)); - throw Slic3r::FileIOError(error_buf); - } - - return n; - }, &data, 0); - } - catch (const version_error& e) - { - // rethrow the exception - throw Slic3r::FileIOError(e.what()); - } - catch (std::exception& e) - { - add_error(e.what()); - return false; - } - - if (res == 0) { - add_error("Error while extracting model data from zip archive"); - return false; - } - - return true; - } - - void _3MF_Importer::_extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) - { - if (stat.m_uncomp_size > 0) { - std::string buffer((size_t)stat.m_uncomp_size, 0); - mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); - if (res == 0) { - add_error("Error while reading cut information data to buffer"); - return; - } - - std::istringstream iss(buffer); // wrap returned xml to istringstream - pt::ptree objects_tree; - pt::read_xml(iss, objects_tree); - - for (const auto& object : objects_tree.get_child("objects")) { - pt::ptree object_tree = object.second; - int obj_idx = object_tree.get(".id", -1); - if (obj_idx <= 0) { - add_error("Found invalid object id"); - continue; - } - - IdToCutObjectInfoMap::iterator object_item = m_cut_object_infos.find(obj_idx); - if (object_item != m_cut_object_infos.end()) { - add_error("Found duplicated cut_object_id"); - continue; - } - - CutObjectBase cut_id; - std::vector connectors; - - for (const auto& obj_cut_info : object_tree) { - if (obj_cut_info.first == "cut_id") { - pt::ptree cut_id_tree = obj_cut_info.second; - cut_id = CutObjectBase(ObjectID( cut_id_tree.get(".id")), - cut_id_tree.get(".check_sum"), - cut_id_tree.get(".connectors_cnt")); - } - if (obj_cut_info.first == "connectors") { - pt::ptree cut_connectors_tree = obj_cut_info.second; - for (const auto& cut_connector : cut_connectors_tree) { - if (cut_connector.first != "connector") - continue; - pt::ptree connector_tree = cut_connector.second; - CutObjectInfo::Connector connector = {connector_tree.get(".volume_id"), - connector_tree.get(".type"), - connector_tree.get(".r_tolerance"), - connector_tree.get(".h_tolerance")}; - connectors.emplace_back(connector); - } - } - } - - CutObjectInfo cut_info {cut_id, connectors}; - m_cut_object_infos.insert({ obj_idx, cut_info }); - } - } - } - - void _3MF_Importer::_extract_print_config_from_archive( - mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, - DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, - const std::string& archive_filename) - { - if (stat.m_uncomp_size > 0) { - std::string buffer((size_t)stat.m_uncomp_size, 0); - mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); - if (res == 0) { - add_error("Error while reading config data to buffer"); - return; - } - //FIXME Loading a "will be one day a legacy format" of configuration in a form of a G-code comment. - // Each config line is prefixed with a semicolon (G-code comment), that is ugly. - - // Replacing the legacy function with load_from_ini_string_commented leads to issues when - // parsing 3MFs from before PrusaSlicer 2.0.0 (which can have duplicated entries in the INI. - // See https://github.com/prusa3d/PrusaSlicer/issues/7155. We'll revert it for now. - //config_substitutions.substitutions = config.load_from_ini_string_commented(std::move(buffer), config_substitutions.rule); - ConfigBase::load_from_gcode_string_legacy(config, buffer.data(), config_substitutions); - } - } - - void _3MF_Importer::_extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) - { - if (stat.m_uncomp_size > 0) { - std::string buffer((size_t)stat.m_uncomp_size, 0); - mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); - if (res == 0) { - add_error("Error while reading layer heights profile data to buffer"); - return; - } - - if (buffer.back() == '\n') - buffer.pop_back(); - - std::vector objects; - boost::split(objects, buffer, boost::is_any_of("\n"), boost::token_compress_off); - - for (const std::string& object : objects) { - std::vector object_data; - boost::split(object_data, object, boost::is_any_of("|"), boost::token_compress_off); - if (object_data.size() != 2) { - add_error("Error while reading object data"); - continue; - } - - std::vector object_data_id; - boost::split(object_data_id, object_data[0], boost::is_any_of("="), boost::token_compress_off); - if (object_data_id.size() != 2) { - add_error("Error while reading object id"); - continue; - } - - int object_id = std::atoi(object_data_id[1].c_str()); - if (object_id == 0) { - add_error("Found invalid object id"); - continue; - } - - IdToLayerHeightsProfileMap::iterator object_item = m_layer_heights_profiles.find(object_id); - if (object_item != m_layer_heights_profiles.end()) { - add_error("Found duplicated layer heights profile"); - continue; - } - - std::vector object_data_profile; - boost::split(object_data_profile, object_data[1], boost::is_any_of(";"), boost::token_compress_off); - if (object_data_profile.size() <= 4 || object_data_profile.size() % 2 != 0) { - add_error("Found invalid layer heights profile"); - continue; - } - - std::vector profile; - profile.reserve(object_data_profile.size()); - - for (const std::string& value : object_data_profile) { - profile.push_back((coordf_t)std::atof(value.c_str())); - } - - m_layer_heights_profiles.insert({ object_id, profile }); - } - } - } - - void _3MF_Importer::_extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) - { - if (stat.m_uncomp_size > 0) { - std::string buffer((size_t)stat.m_uncomp_size, 0); - mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); - if (res == 0) { - add_error("Error while reading layer config ranges data to buffer"); - return; - } - - std::istringstream iss(buffer); // wrap returned xml to istringstream - pt::ptree objects_tree; - pt::read_xml(iss, objects_tree); - - for (const auto& object : objects_tree.get_child("objects")) { - pt::ptree object_tree = object.second; - int obj_idx = object_tree.get(".id", -1); - if (obj_idx <= 0) { - add_error("Found invalid object id"); - continue; - } - - IdToLayerConfigRangesMap::iterator object_item = m_layer_config_ranges.find(obj_idx); - if (object_item != m_layer_config_ranges.end()) { - add_error("Found duplicated layer config range"); - continue; - } - - t_layer_config_ranges config_ranges; - - for (const auto& range : object_tree) { - if (range.first != "range") - continue; - pt::ptree range_tree = range.second; - double min_z = range_tree.get(".min_z"); - double max_z = range_tree.get(".max_z"); - - // get Z range information - DynamicPrintConfig config; - - for (const auto& option : range_tree) { - if (option.first != "option") - continue; - std::string opt_key = option.second.get(".opt_key"); - std::string value = option.second.data(); - config.set_deserialize(opt_key, value, config_substitutions); - } - - config_ranges[{ min_z, max_z }].assign_config(std::move(config)); - } - - if (!config_ranges.empty()) - m_layer_config_ranges.insert({ obj_idx, std::move(config_ranges) }); - } - } - } - - void _3MF_Importer::_extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) - { - if (stat.m_uncomp_size > 0) { - std::string buffer((size_t)stat.m_uncomp_size, 0); - mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); - if (res == 0) { - add_error("Error while reading sla support points data to buffer"); - return; - } - - if (buffer.back() == '\n') - buffer.pop_back(); - - std::vector objects; - boost::split(objects, buffer, boost::is_any_of("\n"), boost::token_compress_off); - - // Info on format versioning - see 3mf.hpp - int version = 0; - std::string key("support_points_format_version="); - if (!objects.empty() && objects[0].find(key) != std::string::npos) { - objects[0].erase(objects[0].begin(), objects[0].begin() + long(key.size())); // removes the string - version = std::stoi(objects[0]); - objects.erase(objects.begin()); // pop the header - } - - for (const std::string& object : objects) { - std::vector object_data; - boost::split(object_data, object, boost::is_any_of("|"), boost::token_compress_off); - - if (object_data.size() != 2) { - add_error("Error while reading object data"); - continue; - } - - std::vector object_data_id; - boost::split(object_data_id, object_data[0], boost::is_any_of("="), boost::token_compress_off); - if (object_data_id.size() != 2) { - add_error("Error while reading object id"); - continue; - } - - int object_id = std::atoi(object_data_id[1].c_str()); - if (object_id == 0) { - add_error("Found invalid object id"); - continue; - } - - IdToSlaSupportPointsMap::iterator object_item = m_sla_support_points.find(object_id); - if (object_item != m_sla_support_points.end()) { - add_error("Found duplicated SLA support points"); - continue; - } - - std::vector object_data_points; - boost::split(object_data_points, object_data[1], boost::is_any_of(" "), boost::token_compress_off); - - std::vector sla_support_points; - - if (version == 0) { - for (unsigned int i=0; i 0) { - std::string buffer(size_t(stat.m_uncomp_size), 0); - mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); - if (res == 0) { - add_error("Error while reading sla support points data to buffer"); - return; - } - - if (buffer.back() == '\n') - buffer.pop_back(); - - std::vector objects; - boost::split(objects, buffer, boost::is_any_of("\n"), boost::token_compress_off); - - // Info on format versioning - see 3mf.hpp - int version = 0; - std::string key("drain_holes_format_version="); - if (!objects.empty() && objects[0].find(key) != std::string::npos) { - objects[0].erase(objects[0].begin(), objects[0].begin() + long(key.size())); // removes the string - version = std::stoi(objects[0]); - objects.erase(objects.begin()); // pop the header - } - - for (const std::string& object : objects) { - std::vector object_data; - boost::split(object_data, object, boost::is_any_of("|"), boost::token_compress_off); - - if (object_data.size() != 2) { - add_error("Error while reading object data"); - continue; - } - - std::vector object_data_id; - boost::split(object_data_id, object_data[0], boost::is_any_of("="), boost::token_compress_off); - if (object_data_id.size() != 2) { - add_error("Error while reading object id"); - continue; - } - - int object_id = std::atoi(object_data_id[1].c_str()); - if (object_id == 0) { - add_error("Found invalid object id"); - continue; - } - - IdToSlaDrainHolesMap::iterator object_item = m_sla_drain_holes.find(object_id); - if (object_item != m_sla_drain_holes.end()) { - add_error("Found duplicated SLA drain holes"); - continue; - } - - std::vector object_data_points; - boost::split(object_data_points, object_data[1], boost::is_any_of(" "), boost::token_compress_off); - - sla::DrainHoles sla_drain_holes; - - if (version == 1) { - for (unsigned int i=0; i 0) { - std::string buffer((size_t)stat.m_uncomp_size, 0); - mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); - if (res == 0) { - add_error("Error while reading custom Gcodes per height data to buffer"); - return; - } - - std::istringstream iss(buffer); // wrap returned xml to istringstream - pt::ptree main_tree; - pt::read_xml(iss, main_tree); - - if (main_tree.front().first != "custom_gcodes_per_print_z") - return; - pt::ptree code_tree = main_tree.front().second; - - m_model->custom_gcode_per_print_z.gcodes.clear(); - - for (const auto& code : code_tree) { - if (code.first == "mode") { - pt::ptree tree = code.second; - std::string mode = tree.get(".value"); - m_model->custom_gcode_per_print_z.mode = mode == CustomGCode::SingleExtruderMode ? CustomGCode::Mode::SingleExtruder : - mode == CustomGCode::MultiAsSingleMode ? CustomGCode::Mode::MultiAsSingle : - CustomGCode::Mode::MultiExtruder; - } - if (code.first != "code") - continue; - - pt::ptree tree = code.second; - double print_z = tree.get (".print_z" ); - int extruder = tree.get (".extruder"); - std::string color = tree.get (".color" ); - - CustomGCode::Type type; - std::string extra; - pt::ptree attr_tree = tree.find("")->second; - if (attr_tree.find("type") == attr_tree.not_found()) { - // It means that data was saved in old version (2.2.0 and older) of PrusaSlicer - // read old data ... - std::string gcode = tree.get (".gcode"); - // ... and interpret them to the new data - type = gcode == "M600" ? CustomGCode::ColorChange : - gcode == "M601" ? CustomGCode::PausePrint : - gcode == "tool_change" ? CustomGCode::ToolChange : CustomGCode::Custom; - extra = type == CustomGCode::PausePrint ? color : - type == CustomGCode::Custom ? gcode : ""; - } - else { - type = static_cast(tree.get(".type")); - extra = tree.get(".extra"); - } - m_model->custom_gcode_per_print_z.gcodes.push_back(CustomGCode::Item{print_z, type, extruder, color, extra}) ; - } - } - } - - void _3MF_Importer::_handle_start_model_xml_element(const char* name, const char** attributes) - { - if (m_xml_parser == nullptr) - return; - - bool res = true; - unsigned int num_attributes = (unsigned int)XML_GetSpecifiedAttributeCount(m_xml_parser); - - if (::strcmp(MODEL_TAG, name) == 0) - res = _handle_start_model(attributes, num_attributes); - else if (::strcmp(RESOURCES_TAG, name) == 0) - res = _handle_start_resources(attributes, num_attributes); - else if (::strcmp(OBJECT_TAG, name) == 0) - res = _handle_start_object(attributes, num_attributes); - else if (::strcmp(MESH_TAG, name) == 0) - res = _handle_start_mesh(attributes, num_attributes); - else if (::strcmp(VERTICES_TAG, name) == 0) - res = _handle_start_vertices(attributes, num_attributes); - else if (::strcmp(VERTEX_TAG, name) == 0) - res = _handle_start_vertex(attributes, num_attributes); - else if (::strcmp(TRIANGLES_TAG, name) == 0) - res = _handle_start_triangles(attributes, num_attributes); - else if (::strcmp(TRIANGLE_TAG, name) == 0) - res = _handle_start_triangle(attributes, num_attributes); - else if (::strcmp(COMPONENTS_TAG, name) == 0) - res = _handle_start_components(attributes, num_attributes); - else if (::strcmp(COMPONENT_TAG, name) == 0) - res = _handle_start_component(attributes, num_attributes); - else if (::strcmp(BUILD_TAG, name) == 0) - res = _handle_start_build(attributes, num_attributes); - else if (::strcmp(ITEM_TAG, name) == 0) - res = _handle_start_item(attributes, num_attributes); - else if (::strcmp(METADATA_TAG, name) == 0) - res = _handle_start_metadata(attributes, num_attributes); - - if (!res) - _stop_xml_parser(); - } - - void _3MF_Importer::_handle_end_model_xml_element(const char* name) - { - if (m_xml_parser == nullptr) - return; - - bool res = true; - - if (::strcmp(MODEL_TAG, name) == 0) - res = _handle_end_model(); - else if (::strcmp(RESOURCES_TAG, name) == 0) - res = _handle_end_resources(); - else if (::strcmp(OBJECT_TAG, name) == 0) - res = _handle_end_object(); - else if (::strcmp(MESH_TAG, name) == 0) - res = _handle_end_mesh(); - else if (::strcmp(VERTICES_TAG, name) == 0) - res = _handle_end_vertices(); - else if (::strcmp(VERTEX_TAG, name) == 0) - res = _handle_end_vertex(); - else if (::strcmp(TRIANGLES_TAG, name) == 0) - res = _handle_end_triangles(); - else if (::strcmp(TRIANGLE_TAG, name) == 0) - res = _handle_end_triangle(); - else if (::strcmp(COMPONENTS_TAG, name) == 0) - res = _handle_end_components(); - else if (::strcmp(COMPONENT_TAG, name) == 0) - res = _handle_end_component(); - else if (::strcmp(BUILD_TAG, name) == 0) - res = _handle_end_build(); - else if (::strcmp(ITEM_TAG, name) == 0) - res = _handle_end_item(); - else if (::strcmp(METADATA_TAG, name) == 0) - res = _handle_end_metadata(); - - if (!res) - _stop_xml_parser(); - } - - void _3MF_Importer::_handle_model_xml_characters(const XML_Char* s, int len) - { - m_curr_characters.append(s, len); - } - - void _3MF_Importer::_handle_start_config_xml_element(const char* name, const char** attributes) - { - if (m_xml_parser == nullptr) - return; - - bool res = true; - unsigned int num_attributes = (unsigned int)XML_GetSpecifiedAttributeCount(m_xml_parser); - - if (::strcmp(CONFIG_TAG, name) == 0) - res = _handle_start_config(attributes, num_attributes); - else if (::strcmp(OBJECT_TAG, name) == 0) - res = _handle_start_config_object(attributes, num_attributes); - else if (::strcmp(VOLUME_TAG, name) == 0) - res = _handle_start_config_volume(attributes, num_attributes); - else if (::strcmp(MESH_TAG, name) == 0) - res = _handle_start_config_volume_mesh(attributes, num_attributes); - else if (::strcmp(METADATA_TAG, name) == 0) - res = _handle_start_config_metadata(attributes, num_attributes); - - if (!res) - _stop_xml_parser(); - } - - void _3MF_Importer::_handle_end_config_xml_element(const char* name) - { - if (m_xml_parser == nullptr) - return; - - bool res = true; - - if (::strcmp(CONFIG_TAG, name) == 0) - res = _handle_end_config(); - else if (::strcmp(OBJECT_TAG, name) == 0) - res = _handle_end_config_object(); - else if (::strcmp(VOLUME_TAG, name) == 0) - res = _handle_end_config_volume(); - else if (::strcmp(MESH_TAG, name) == 0) - res = _handle_end_config_volume_mesh(); - else if (::strcmp(METADATA_TAG, name) == 0) - res = _handle_end_config_metadata(); - - if (!res) - _stop_xml_parser(); - } - - bool _3MF_Importer::_handle_start_model(const char** attributes, unsigned int num_attributes) - { - m_unit_factor = get_unit_factor(get_attribute_value_string(attributes, num_attributes, UNIT_ATTR)); - return true; - } - - bool _3MF_Importer::_handle_end_model() - { - // deletes all non-built or non-instanced objects - for (const IdToModelObjectMap::value_type& object : m_objects) { - if (object.second >= int(m_model->objects.size())) { - add_error("Unable to find object"); - return false; - } - ModelObject *model_object = m_model->objects[object.second]; - if (model_object != nullptr && model_object->instances.size() == 0) - m_model->delete_object(model_object); - } - - if (m_version == 0) { - // if the 3mf was not produced by PrusaSlicer and there is only one object, - // set the object name to match the filename - if (m_model->objects.size() == 1) - m_model->objects.front()->name = m_name; - } - - // applies instances' matrices - for (Instance& instance : m_instances) { - if (instance.instance != nullptr && instance.instance->get_object() != nullptr) - // apply the transform to the instance - _apply_transform(*instance.instance, instance.transform); - } - - return true; - } - - bool _3MF_Importer::_handle_start_resources(const char** attributes, unsigned int num_attributes) - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_end_resources() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_object(const char** attributes, unsigned int num_attributes) - { - // reset current data - m_curr_object.reset(); - - if (is_valid_object_type(get_attribute_value_string(attributes, num_attributes, TYPE_ATTR))) { - // create new object (it may be removed later if no instances are generated from it) - m_curr_object.model_object_idx = (int)m_model->objects.size(); - m_curr_object.object = m_model->add_object(); - if (m_curr_object.object == nullptr) { - add_error("Unable to create object"); - return false; - } - - // set object data - m_curr_object.object->name = get_attribute_value_string(attributes, num_attributes, NAME_ATTR); - if (m_curr_object.object->name.empty()) - m_curr_object.object->name = m_name + "_" + std::to_string(m_model->objects.size()); - - m_curr_object.id = get_attribute_value_int(attributes, num_attributes, ID_ATTR); - } - - return true; - } - - bool _3MF_Importer::_handle_end_object() - { - if (m_curr_object.object != nullptr) { - if (m_curr_object.geometry.empty()) { - // no geometry defined - // remove the object from the model - m_model->delete_object(m_curr_object.object); - - if (m_curr_object.components.empty()) { - // no components defined -> invalid object, delete it - IdToModelObjectMap::iterator object_item = m_objects.find(m_curr_object.id); - if (object_item != m_objects.end()) - m_objects.erase(object_item); - - IdToAliasesMap::iterator alias_item = m_objects_aliases.find(m_curr_object.id); - if (alias_item != m_objects_aliases.end()) - m_objects_aliases.erase(alias_item); - } - else - // adds components to aliases - m_objects_aliases.insert({ m_curr_object.id, m_curr_object.components }); - } - else { - // geometry defined, store it for later use - m_geometries.insert({ m_curr_object.id, std::move(m_curr_object.geometry) }); - - // stores the object for later use - if (m_objects.find(m_curr_object.id) == m_objects.end()) { - m_objects.insert({ m_curr_object.id, m_curr_object.model_object_idx }); - m_objects_aliases.insert({ m_curr_object.id, { 1, Component(m_curr_object.id) } }); // aliases itself - } - else { - add_error("Found object with duplicate id"); - return false; - } - } - } - - return true; - } - - bool _3MF_Importer::_handle_start_mesh(const char** attributes, unsigned int num_attributes) - { - // reset current geometry - m_curr_object.geometry.reset(); - return true; - } - - bool _3MF_Importer::_handle_end_mesh() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_vertices(const char** attributes, unsigned int num_attributes) - { - // reset current vertices - m_curr_object.geometry.vertices.clear(); - return true; - } - - bool _3MF_Importer::_handle_end_vertices() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_vertex(const char** attributes, unsigned int num_attributes) - { - // appends the vertex coordinates - // missing values are set equal to ZERO - m_curr_object.geometry.vertices.emplace_back( - m_unit_factor * get_attribute_value_float(attributes, num_attributes, X_ATTR), - m_unit_factor * get_attribute_value_float(attributes, num_attributes, Y_ATTR), - m_unit_factor * get_attribute_value_float(attributes, num_attributes, Z_ATTR)); - return true; - } - - bool _3MF_Importer::_handle_end_vertex() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_triangles(const char** attributes, unsigned int num_attributes) - { - // reset current triangles - m_curr_object.geometry.triangles.clear(); - return true; - } - - bool _3MF_Importer::_handle_end_triangles() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_triangle(const char** attributes, unsigned int num_attributes) - { - // we are ignoring the following attributes: - // p1 - // p2 - // p3 - // pid - // see specifications - - // appends the triangle's vertices indices - // missing values are set equal to ZERO - m_curr_object.geometry.triangles.emplace_back( - get_attribute_value_int(attributes, num_attributes, V1_ATTR), - get_attribute_value_int(attributes, num_attributes, V2_ATTR), - get_attribute_value_int(attributes, num_attributes, V3_ATTR)); - - m_curr_object.geometry.custom_supports.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SUPPORTS_ATTR)); - m_curr_object.geometry.custom_seam.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); - m_curr_object.geometry.mmu_segmentation.push_back(get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); - return true; - } - - bool _3MF_Importer::_handle_end_triangle() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_components(const char** attributes, unsigned int num_attributes) - { - // reset current components - m_curr_object.components.clear(); - return true; - } - - bool _3MF_Importer::_handle_end_components() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_component(const char** attributes, unsigned int num_attributes) - { - int object_id = get_attribute_value_int(attributes, num_attributes, OBJECTID_ATTR); - Transform3d transform = get_transform_from_3mf_specs_string(get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); - - IdToModelObjectMap::iterator object_item = m_objects.find(object_id); - if (object_item == m_objects.end()) { - IdToAliasesMap::iterator alias_item = m_objects_aliases.find(object_id); - if (alias_item == m_objects_aliases.end()) { - add_error("Found component with invalid object id"); - return false; - } - } - - m_curr_object.components.emplace_back(object_id, transform); - - return true; - } - - bool _3MF_Importer::_handle_end_component() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_build(const char** attributes, unsigned int num_attributes) - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_end_build() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_item(const char** attributes, unsigned int num_attributes) - { - // we are ignoring the following attributes - // thumbnail - // partnumber - // pid - // pindex - // see specifications - - int object_id = get_attribute_value_int(attributes, num_attributes, OBJECTID_ATTR); - Transform3d transform = get_transform_from_3mf_specs_string(get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); - int printable = get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR); - - return _create_object_instance(object_id, transform, printable, 1); - } - - bool _3MF_Importer::_handle_end_item() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_metadata(const char** attributes, unsigned int num_attributes) - { - m_curr_characters.clear(); - - std::string name = get_attribute_value_string(attributes, num_attributes, NAME_ATTR); - if (!name.empty()) - m_curr_metadata_name = name; - - return true; - } - - inline static void check_painting_version(unsigned int loaded_version, unsigned int highest_supported_version, const std::string &error_msg) - { - if (loaded_version > highest_supported_version) - throw version_error(error_msg); - } - - bool _3MF_Importer::_handle_end_metadata() - { - if (m_curr_metadata_name == SLIC3RPE_3MF_VERSION) { - m_version = (unsigned int)atoi(m_curr_characters.c_str()); - if (m_check_version && (m_version > VERSION_3MF_COMPATIBLE)) { - // std::string msg = _(L("The selected 3mf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible.")); - // throw version_error(msg.c_str()); - const std::string msg = (boost::format(_(L("The selected 3mf file has been saved with a newer version of %1% and is not compatible."))) % std::string(SLIC3R_APP_NAME)).str(); - throw version_error(msg); - } - } else if (m_curr_metadata_name == "Application") { - // Generator application of the 3MF. - // SLIC3R_APP_KEY - SLIC3R_VERSION - if (boost::starts_with(m_curr_characters, "PrusaSlicer-")) - m_prusaslicer_generator_version = Semver::parse(m_curr_characters.substr(12)); - } else if (m_curr_metadata_name == SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION) { - m_fdm_supports_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); - check_painting_version(m_fdm_supports_painting_version, FDM_SUPPORTS_PAINTING_VERSION, - _(L("The selected 3MF contains FDM supports painted object using a newer version of PrusaSlicer and is not compatible."))); - } else if (m_curr_metadata_name == SLIC3RPE_SEAM_PAINTING_VERSION) { - m_seam_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); - check_painting_version(m_seam_painting_version, SEAM_PAINTING_VERSION, - _(L("The selected 3MF contains seam painted object using a newer version of PrusaSlicer and is not compatible."))); - } else if (m_curr_metadata_name == SLIC3RPE_MM_PAINTING_VERSION) { - m_mm_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); - check_painting_version(m_mm_painting_version, MM_PAINTING_VERSION, - _(L("The selected 3MF contains multi-material painted object using a newer version of PrusaSlicer and is not compatible."))); - } - - return true; - } - - bool _3MF_Importer::_create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter) - { - static const unsigned int MAX_RECURSIONS = 10; - - // escape from circular aliasing - if (recur_counter > MAX_RECURSIONS) { - add_error("Too many recursions"); - return false; - } - - IdToAliasesMap::iterator it = m_objects_aliases.find(object_id); - if (it == m_objects_aliases.end()) { - add_error("Found item with invalid object id"); - return false; - } - - if (it->second.size() == 1 && it->second[0].object_id == object_id) { - // aliasing to itself - - IdToModelObjectMap::iterator object_item = m_objects.find(object_id); - if (object_item == m_objects.end() || object_item->second == -1) { - add_error("Found invalid object"); - return false; - } - else { - ModelInstance* instance = m_model->objects[object_item->second]->add_instance(); - if (instance == nullptr) { - add_error("Unable to add object instance"); - return false; - } - instance->printable = printable; - - m_instances.emplace_back(instance, transform); - } - } - else { - // recursively process nested components - for (const Component& component : it->second) { - if (!_create_object_instance(component.object_id, transform * component.transform, printable, recur_counter + 1)) - return false; - } - } - - return true; - } - - void _3MF_Importer::_apply_transform(ModelInstance& instance, const Transform3d& transform) - { - Slic3r::Geometry::Transformation t(transform); - // invalid scale value, return - if (!t.get_scaling_factor().all()) - return; - - instance.set_transformation(t); - } - - bool _3MF_Importer::_handle_start_config(const char** attributes, unsigned int num_attributes) - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_end_config() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_config_object(const char** attributes, unsigned int num_attributes) - { - int object_id = get_attribute_value_int(attributes, num_attributes, ID_ATTR); - IdToMetadataMap::iterator object_item = m_objects_metadata.find(object_id); - if (object_item != m_objects_metadata.end()) { - add_error("Found duplicated object id"); - return false; - } - - // Added because of github #3435, currently not used by PrusaSlicer - // int instances_count_id = get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR); - - m_objects_metadata.insert({ object_id, ObjectMetadata() }); - m_curr_config.object_id = object_id; - return true; - } - - bool _3MF_Importer::_handle_end_config_object() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_config_volume(const char** attributes, unsigned int num_attributes) - { - IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); - if (object == m_objects_metadata.end()) { - add_error("Cannot assign volume to a valid object"); - return false; - } - - m_curr_config.volume_id = (int)object->second.volumes.size(); - - unsigned int first_triangle_id = (unsigned int)get_attribute_value_int(attributes, num_attributes, FIRST_TRIANGLE_ID_ATTR); - unsigned int last_triangle_id = (unsigned int)get_attribute_value_int(attributes, num_attributes, LAST_TRIANGLE_ID_ATTR); - - object->second.volumes.emplace_back(first_triangle_id, last_triangle_id); - return true; - } - - bool _3MF_Importer::_handle_start_config_volume_mesh(const char** attributes, unsigned int num_attributes) - { - IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); - if (object == m_objects_metadata.end()) { - add_error("Cannot assign volume mesh to a valid object"); - return false; - } - if (object->second.volumes.empty()) { - add_error("Cannot assign mesh to a valid olume"); - return false; - } - - ObjectMetadata::VolumeMetadata& volume = object->second.volumes.back(); - - int edges_fixed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_EDGES_FIXED ); - int degenerate_facets = get_attribute_value_int(attributes, num_attributes, MESH_STAT_DEGENERATED_FACETS); - int facets_removed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_FACETS_REMOVED ); - int facets_reversed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_FACETS_RESERVED ); - int backwards_edges = get_attribute_value_int(attributes, num_attributes, MESH_STAT_BACKWARDS_EDGES ); - - volume.mesh_stats = { edges_fixed, degenerate_facets, facets_removed, facets_reversed, backwards_edges }; - - return true; - } - - bool _3MF_Importer::_handle_end_config_volume() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_end_config_volume_mesh() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_config_metadata(const char** attributes, unsigned int num_attributes) - { - IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); - if (object == m_objects_metadata.end()) { - add_error("Cannot assign metadata to valid object id"); - return false; - } - - std::string type = get_attribute_value_string(attributes, num_attributes, TYPE_ATTR); - std::string key = get_attribute_value_string(attributes, num_attributes, KEY_ATTR); - std::string value = get_attribute_value_string(attributes, num_attributes, VALUE_ATTR); - - if (type == OBJECT_TYPE) - object->second.metadata.emplace_back(key, value); - else if (type == VOLUME_TYPE) { - if (size_t(m_curr_config.volume_id) < object->second.volumes.size()) - object->second.volumes[m_curr_config.volume_id].metadata.emplace_back(key, value); - } - else { - add_error("Found invalid metadata type"); - return false; - } - - return true; - } - - bool _3MF_Importer::_handle_end_config_metadata() - { - // do nothing - return true; - } - - bool _3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions) - { - if (!object.volumes.empty()) { - add_error("Found invalid volumes count"); - return false; - } - - unsigned int geo_tri_count = (unsigned int)geometry.triangles.size(); - unsigned int renamed_volumes_count = 0; - - for (const ObjectMetadata::VolumeMetadata& volume_data : volumes) { - if (geo_tri_count <= volume_data.first_triangle_id || geo_tri_count <= volume_data.last_triangle_id || volume_data.last_triangle_id < volume_data.first_triangle_id) { - add_error("Found invalid triangle id"); - return false; - } - - Transform3d volume_matrix_to_object = Transform3d::Identity(); - bool has_transform = false; - // extract the volume transformation from the volume's metadata, if present - for (const Metadata& metadata : volume_data.metadata) { - if (metadata.key == MATRIX_KEY) { - volume_matrix_to_object = Slic3r::Geometry::transform3d_from_string(metadata.value); - has_transform = ! volume_matrix_to_object.isApprox(Transform3d::Identity(), 1e-10); - break; - } - } - - // splits volume out of imported geometry - indexed_triangle_set its; - its.indices.assign(geometry.triangles.begin() + volume_data.first_triangle_id, geometry.triangles.begin() + volume_data.last_triangle_id + 1); - const size_t triangles_count = its.indices.size(); - if (triangles_count == 0) { - add_error("An empty triangle mesh found"); - return false; - } - - { - int min_id = its.indices.front()[0]; - int max_id = min_id; - for (const Vec3i& face : its.indices) { - for (const int tri_id : face) { - if (tri_id < 0 || tri_id >= int(geometry.vertices.size())) { - add_error("Found invalid vertex id"); - return false; - } - min_id = std::min(min_id, tri_id); - max_id = std::max(max_id, tri_id); - } - } - its.vertices.assign(geometry.vertices.begin() + min_id, geometry.vertices.begin() + max_id + 1); - - // rebase indices to the current vertices list - for (Vec3i& face : its.indices) - for (int& tri_id : face) - tri_id -= min_id; - } - - if (m_prusaslicer_generator_version && - *m_prusaslicer_generator_version >= *Semver::parse("2.4.0-alpha1") && - *m_prusaslicer_generator_version < *Semver::parse("2.4.0-alpha3")) - // PrusaSlicer 2.4.0-alpha2 contained a bug, where all vertices of a single object were saved for each volume the object contained. - // Remove the vertices, that are not referenced by any face. - its_compactify_vertices(its, true); - - TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats); - - if (m_version == 0) { - // if the 3mf was not produced by PrusaSlicer and there is only one instance, - // bake the transformation into the geometry to allow the reload from disk command - // to work properly - if (object.instances.size() == 1) { - triangle_mesh.transform(object.instances.front()->get_transformation().get_matrix(), false); - object.instances.front()->set_transformation(Slic3r::Geometry::Transformation()); - //FIXME do the mesh fixing? - } - } - if (triangle_mesh.volume() < 0) - triangle_mesh.flip_triangles(); - - ModelVolume* volume = object.add_volume(std::move(triangle_mesh)); - // stores the volume matrix taken from the metadata, if present - if (has_transform) - volume->source.transform = Slic3r::Geometry::Transformation(volume_matrix_to_object); - - // recreate custom supports, seam and mmu segmentation from previously loaded attribute - volume->supported_facets.reserve(triangles_count); - volume->seam_facets.reserve(triangles_count); - volume->mmu_segmentation_facets.reserve(triangles_count); - for (size_t i=0; isupported_facets.set_triangle_from_string(i, geometry.custom_supports[index]); - if (! geometry.custom_seam[index].empty()) - volume->seam_facets.set_triangle_from_string(i, geometry.custom_seam[index]); - if (! geometry.mmu_segmentation[index].empty()) - volume->mmu_segmentation_facets.set_triangle_from_string(i, geometry.mmu_segmentation[index]); - } - volume->supported_facets.shrink_to_fit(); - volume->seam_facets.shrink_to_fit(); - volume->mmu_segmentation_facets.shrink_to_fit(); - - // apply the remaining volume's metadata - for (const Metadata& metadata : volume_data.metadata) { - if (metadata.key == NAME_KEY) - volume->name = metadata.value; - else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1")) - volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); - else if (metadata.key == VOLUME_TYPE_KEY) - volume->set_type(ModelVolume::type_from_string(metadata.value)); - else if (metadata.key == SOURCE_FILE_KEY) - volume->source.input_file = metadata.value; - else if (metadata.key == SOURCE_OBJECT_ID_KEY) - volume->source.object_idx = ::atoi(metadata.value.c_str()); - else if (metadata.key == SOURCE_VOLUME_ID_KEY) - volume->source.volume_idx = ::atoi(metadata.value.c_str()); - else if (metadata.key == SOURCE_OFFSET_X_KEY) - volume->source.mesh_offset.x() = ::atof(metadata.value.c_str()); - else if (metadata.key == SOURCE_OFFSET_Y_KEY) - volume->source.mesh_offset.y() = ::atof(metadata.value.c_str()); - else if (metadata.key == SOURCE_OFFSET_Z_KEY) - volume->source.mesh_offset.z() = ::atof(metadata.value.c_str()); - else if (metadata.key == SOURCE_IN_INCHES_KEY) - volume->source.is_converted_from_inches = metadata.value == "1"; - else if (metadata.key == SOURCE_IN_METERS_KEY) - volume->source.is_converted_from_meters = metadata.value == "1"; -#if ENABLE_RELOAD_FROM_DISK_REWORK - else if (metadata.key == SOURCE_IS_BUILTIN_VOLUME_KEY) - volume->source.is_from_builtin_objects = metadata.value == "1"; -#endif // ENABLE_RELOAD_FROM_DISK_REWORK - else - volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); - } - - // this may happen for 3mf saved by 3rd part softwares - if (volume->name.empty()) { - volume->name = object.name; - if (renamed_volumes_count > 0) - volume->name += "_" + std::to_string(renamed_volumes_count + 1); - ++renamed_volumes_count; - } - } - - return true; - } - - void XMLCALL _3MF_Importer::_handle_start_model_xml_element(void* userData, const char* name, const char** attributes) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_start_model_xml_element(name, attributes); - } - - void XMLCALL _3MF_Importer::_handle_end_model_xml_element(void* userData, const char* name) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_end_model_xml_element(name); - } - - void XMLCALL _3MF_Importer::_handle_model_xml_characters(void* userData, const XML_Char* s, int len) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_model_xml_characters(s, len); - } - - void XMLCALL _3MF_Importer::_handle_start_config_xml_element(void* userData, const char* name, const char** attributes) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_start_config_xml_element(name, attributes); - } - - void XMLCALL _3MF_Importer::_handle_end_config_xml_element(void* userData, const char* name) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_end_config_xml_element(name); - } - - class _3MF_Exporter : public _3MF_Base - { - struct BuildItem - { - unsigned int id; - Transform3d transform; - bool printable; - - BuildItem(unsigned int id, const Transform3d& transform, const bool printable) - : id(id) - , transform(transform) - , printable(printable) - { - } - }; - - struct Offsets - { - unsigned int first_vertex_id; - unsigned int first_triangle_id; - unsigned int last_triangle_id; - - Offsets(unsigned int first_vertex_id) - : first_vertex_id(first_vertex_id) - , first_triangle_id(-1) - , last_triangle_id(-1) - { - } - }; - - typedef std::map VolumeToOffsetsMap; - - struct ObjectData - { - ModelObject* object; - VolumeToOffsetsMap volumes_offsets; - - explicit ObjectData(ModelObject* object) - : object(object) - { - } - }; - - typedef std::vector BuildItemsList; - typedef std::map IdToObjectDataMap; - - bool m_fullpath_sources{ true }; - bool m_zip64 { true }; - - public: - bool save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64); - - private: - bool _save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data); - bool _add_content_types_file_to_archive(mz_zip_archive& archive); - bool _add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data); - bool _add_relationships_file_to_archive(mz_zip_archive& archive); - bool _add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data); - bool _add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets); - bool _add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets); - bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items); - bool _add_cut_information_file_to_archive(mz_zip_archive& archive, Model& model); - bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model); - bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model); - bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model); - bool _add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model); - bool _add_print_config_file_to_archive(mz_zip_archive& archive, const DynamicPrintConfig &config); - bool _add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, const IdToObjectDataMap &objects_data); - bool _add_custom_gcode_per_print_z_file_to_archive(mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config); - }; - - bool _3MF_Exporter::save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64) - { - clear_errors(); - m_fullpath_sources = fullpath_sources; - m_zip64 = zip64; - return _save_model_to_file(filename, model, config, thumbnail_data); - } - - bool _3MF_Exporter::_save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data) - { - mz_zip_archive archive; - mz_zip_zero_struct(&archive); - - if (!open_zip_writer(&archive, filename)) { - add_error("Unable to open the file"); - return false; - } - - // Adds content types file ("[Content_Types].xml";). - // The content of this file is the same for each PrusaSlicer 3mf. - if (!_add_content_types_file_to_archive(archive)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - if (thumbnail_data != nullptr && thumbnail_data->is_valid()) { - // Adds the file Metadata/thumbnail.png. - if (!_add_thumbnail_file_to_archive(archive, *thumbnail_data)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - } - - // Adds relationships file ("_rels/.rels"). - // The content of this file is the same for each PrusaSlicer 3mf. - // The relationshis file contains a reference to the geometry file "3D/3dmodel.model", the name was chosen to be compatible with CURA. - if (!_add_relationships_file_to_archive(archive)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds model file ("3D/3dmodel.model"). - // This is the one and only file that contains all the geometry (vertices and triangles) of all ModelVolumes. - IdToObjectDataMap objects_data; - if (!_add_model_file_to_archive(filename, archive, model, objects_data)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds file with information for object cut ("Metadata/Slic3r_PE_cut_information.txt"). - // All information for object cut of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. - // The index differes from the index of an object ID of an object instance of a 3MF file! - if (!_add_cut_information_file_to_archive(archive, model)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds layer height profile file ("Metadata/Slic3r_PE_layer_heights_profile.txt"). - // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. - // The index differes from the index of an object ID of an object instance of a 3MF file! - if (!_add_layer_height_profile_file_to_archive(archive, model)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds layer config ranges file ("Metadata/Slic3r_PE_layer_config_ranges.txt"). - // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. - // The index differes from the index of an object ID of an object instance of a 3MF file! - if (!_add_layer_config_ranges_file_to_archive(archive, model)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds sla support points file ("Metadata/Slic3r_PE_sla_support_points.txt"). - // All sla support points of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. - // The index differes from the index of an object ID of an object instance of a 3MF file! - if (!_add_sla_support_points_file_to_archive(archive, model)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - if (!_add_sla_drain_holes_file_to_archive(archive, model)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - - // Adds custom gcode per height file ("Metadata/Prusa_Slicer_custom_gcode_per_print_z.xml"). - // All custom gcode per height of whole Model are stored here - if (!_add_custom_gcode_per_print_z_file_to_archive(archive, model, config)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds slic3r print config file ("Metadata/Slic3r_PE.config"). - // This file contains the content of FullPrintConfing / SLAFullPrintConfig. - if (config != nullptr) { - if (!_add_print_config_file_to_archive(archive, *config)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - } - - // Adds slic3r model config file ("Metadata/Slic3r_PE_model.config"). - // This file contains all the attributes of all ModelObjects and their ModelVolumes (names, parameter overrides). - // As there is just a single Indexed Triangle Set data stored per ModelObject, offsets of volumes into their respective Indexed Triangle Set data - // is stored here as well. - if (!_add_model_config_file_to_archive(archive, model, objects_data)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - if (!mz_zip_writer_finalize_archive(&archive)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - add_error("Unable to finalize the archive"); - return false; - } - - close_zip_writer(&archive); - - return true; - } - - bool _3MF_Exporter::_add_content_types_file_to_archive(mz_zip_archive& archive) - { - std::stringstream stream; - stream << "\n"; - stream << "\n"; - stream << " \n"; - stream << " \n"; - stream << " \n"; - stream << ""; - - std::string out = stream.str(); - - if (!mz_zip_writer_add_mem(&archive, CONTENT_TYPES_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add content types file to archive"); - return false; - } - - return true; - } - - bool _3MF_Exporter::_add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data) - { - bool res = false; - - size_t png_size = 0; - void* png_data = tdefl_write_image_to_png_file_in_memory_ex((const void*)thumbnail_data.pixels.data(), thumbnail_data.width, thumbnail_data.height, 4, &png_size, MZ_DEFAULT_LEVEL, 1); - if (png_data != nullptr) { - res = mz_zip_writer_add_mem(&archive, THUMBNAIL_FILE.c_str(), (const void*)png_data, png_size, MZ_DEFAULT_COMPRESSION); - mz_free(png_data); - } - - if (!res) - add_error("Unable to add thumbnail file to archive"); - - return res; - } - - bool _3MF_Exporter::_add_relationships_file_to_archive(mz_zip_archive& archive) - { - std::stringstream stream; - stream << "\n"; - stream << "\n"; - stream << " \n"; - stream << " \n"; - stream << ""; - - std::string out = stream.str(); - - if (!mz_zip_writer_add_mem(&archive, RELATIONSHIPS_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add relationships file to archive"); - return false; - } - - return true; - } - - static void reset_stream(std::stringstream &stream) - { - stream.str(""); - stream.clear(); - // https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10 - // Conversion of a floating-point value to text and back is exact as long as at least max_digits10 were used (9 for float, 17 for double). - // It is guaranteed to produce the same floating-point value, even though the intermediate text representation is not exact. - // The default value of std::stream precision is 6 digits only! - stream << std::setprecision(std::numeric_limits::max_digits10); - } - - bool _3MF_Exporter::_add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data) - { - mz_zip_writer_staged_context context; - if (!mz_zip_writer_add_staged_open(&archive, &context, MODEL_FILE.c_str(), - m_zip64 ? - // Maximum expected and allowed 3MF file size is 16GiB. - // This switches the ZIP file to a 64bit mode, which adds a tiny bit of overhead to file records. - (uint64_t(1) << 30) * 16 : - // Maximum expected 3MF file size is 4GB-1. This is a workaround for interoperability with Windows 10 3D model fixing API, see - // GH issue #6193. - (uint64_t(1) << 32) - 1, - nullptr, nullptr, 0, MZ_DEFAULT_COMPRESSION, nullptr, 0, nullptr, 0)) { - add_error("Unable to add model file to archive"); - return false; - } - - { - std::stringstream stream; - reset_stream(stream); - stream << "\n"; - stream << "<" << MODEL_TAG << " unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\" xmlns:slic3rpe=\"http://schemas.slic3r.org/3mf/2017/06\">\n"; - stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_3MF_VERSION << "\">" << VERSION_3MF << "\n"; - - if (model.is_fdm_support_painted()) - stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION << "\">" << FDM_SUPPORTS_PAINTING_VERSION << "\n"; - - if (model.is_seam_painted()) - stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_SEAM_PAINTING_VERSION << "\">" << SEAM_PAINTING_VERSION << "\n"; - - if (model.is_mm_painted()) - stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_MM_PAINTING_VERSION << "\">" << MM_PAINTING_VERSION << "\n"; - - std::string name = xml_escape(boost::filesystem::path(filename).stem().string()); - stream << " <" << METADATA_TAG << " name=\"Title\">" << name << "\n"; - stream << " <" << METADATA_TAG << " name=\"Designer\">" << "\n"; - stream << " <" << METADATA_TAG << " name=\"Description\">" << name << "\n"; - stream << " <" << METADATA_TAG << " name=\"Copyright\">" << "\n"; - stream << " <" << METADATA_TAG << " name=\"LicenseTerms\">" << "\n"; - stream << " <" << METADATA_TAG << " name=\"Rating\">" << "\n"; - std::string date = Slic3r::Utils::utc_timestamp(Slic3r::Utils::get_current_time_utc()); - // keep only the date part of the string - date = date.substr(0, 10); - stream << " <" << METADATA_TAG << " name=\"CreationDate\">" << date << "\n"; - stream << " <" << METADATA_TAG << " name=\"ModificationDate\">" << date << "\n"; - stream << " <" << METADATA_TAG << " name=\"Application\">" << SLIC3R_APP_KEY << "-" << SLIC3R_VERSION << "\n"; - stream << " <" << RESOURCES_TAG << ">\n"; - std::string buf = stream.str(); - if (! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) { - add_error("Unable to add model file to archive"); - return false; - } - } - - // Instance transformations, indexed by the 3MF object ID (which is a linear serialization of all instances of all ModelObjects). - BuildItemsList build_items; - - // The object_id here is a one based identifier of the first instance of a ModelObject in the 3MF file, where - // all the object instances of all ModelObjects are stored and indexed in a 1 based linear fashion. - // Therefore the list of object_ids here may not be continuous. - unsigned int object_id = 1; - for (ModelObject* obj : model.objects) { - if (obj == nullptr) - continue; - - // Index of an object in the 3MF file corresponding to the 1st instance of a ModelObject. - unsigned int curr_id = object_id; - IdToObjectDataMap::iterator object_it = objects_data.insert({ curr_id, ObjectData(obj) }).first; - // Store geometry of all ModelVolumes contained in a single ModelObject into a single 3MF indexed triangle set object. - // object_it->second.volumes_offsets will contain the offsets of the ModelVolumes in that single indexed triangle set. - // object_id will be increased to point to the 1st instance of the next ModelObject. - if (!_add_object_to_model_stream(context, object_id, *obj, build_items, object_it->second.volumes_offsets)) { - add_error("Unable to add object to archive"); - mz_zip_writer_add_staged_finish(&context); - return false; - } - } - - { - std::stringstream stream; - reset_stream(stream); - stream << " \n"; - - // Store the transformations of all the ModelInstances of all ModelObjects, indexed in a linear fashion. - if (!_add_build_to_model_stream(stream, build_items)) { - add_error("Unable to add build to archive"); - mz_zip_writer_add_staged_finish(&context); - return false; - } - - stream << "\n"; - - std::string buf = stream.str(); - - if ((! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) || - ! mz_zip_writer_add_staged_finish(&context)) { - add_error("Unable to add model file to archive"); - return false; - } - } - - return true; - } - - bool _3MF_Exporter::_add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets) - { - std::stringstream stream; - reset_stream(stream); - unsigned int id = 0; - for (const ModelInstance* instance : object.instances) { - assert(instance != nullptr); - if (instance == nullptr) - continue; - - unsigned int instance_id = object_id + id; - stream << " <" << OBJECT_TAG << " id=\"" << instance_id << "\" type=\"model\">\n"; - - if (id == 0) { - std::string buf = stream.str(); - reset_stream(stream); - if ((! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) || - ! _add_mesh_to_object_stream(context, object, volumes_offsets)) { - add_error("Unable to add mesh to archive"); - return false; - } - } - else { - stream << " <" << COMPONENTS_TAG << ">\n"; - stream << " <" << COMPONENT_TAG << " objectid=\"" << object_id << "\"/>\n"; - stream << " \n"; - } - - Transform3d t = instance->get_matrix(); - // instance_id is just a 1 indexed index in build_items. - assert(instance_id == build_items.size() + 1); - build_items.emplace_back(instance_id, t, instance->printable); - - stream << " \n"; - - ++id; - } - - object_id += id; - std::string buf = stream.str(); - return buf.empty() || mz_zip_writer_add_staged_data(&context, buf.data(), buf.size()); - } - -#if EXPORT_3MF_USE_SPIRIT_KARMA_FP - template - struct coordinate_policy_fixed : boost::spirit::karma::real_policies - { - static int floatfield(Num n) { return fmtflags::fixed; } - // Number of decimal digits to maintain float accuracy when storing into a text file and parsing back. - static unsigned precision(Num /* n */) { return std::numeric_limits::max_digits10 + 1; } - // No trailing zeros, thus for fmtflags::fixed usually much less than max_digits10 decimal numbers will be produced. - static bool trailing_zeros(Num /* n */) { return false; } - }; - template - struct coordinate_policy_scientific : coordinate_policy_fixed - { - static int floatfield(Num n) { return fmtflags::scientific; } - }; - // Define a new generator type based on the new coordinate policy. - using coordinate_type_fixed = boost::spirit::karma::real_generator>; - using coordinate_type_scientific = boost::spirit::karma::real_generator>; -#endif // EXPORT_3MF_USE_SPIRIT_KARMA_FP - - bool _3MF_Exporter::_add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets) - { - std::string output_buffer; - output_buffer += " <"; - output_buffer += MESH_TAG; - output_buffer += ">\n <"; - output_buffer += VERTICES_TAG; - output_buffer += ">\n"; - - auto flush = [this, &output_buffer, &context](bool force = false) { - if ((force && ! output_buffer.empty()) || output_buffer.size() >= 65536 * 16) { - if (! mz_zip_writer_add_staged_data(&context, output_buffer.data(), output_buffer.size())) { - add_error("Error during writing or compression"); - return false; - } - output_buffer.clear(); - } - return true; - }; - - auto format_coordinate = [](float f, char *buf) -> char* { - assert(is_decimal_separator_point()); -#if EXPORT_3MF_USE_SPIRIT_KARMA_FP - // Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, - // https://github.com/boostorg/spirit/pull/586 - // where the exported string is one digit shorter than it should be to guarantee lossless round trip. - // The code is left here for the ocasion boost guys improve. - coordinate_type_fixed const coordinate_fixed = coordinate_type_fixed(); - coordinate_type_scientific const coordinate_scientific = coordinate_type_scientific(); - // Format "f" in a fixed format. - char *ptr = buf; - boost::spirit::karma::generate(ptr, coordinate_fixed, f); - // Format "f" in a scientific format. - char *ptr2 = ptr; - boost::spirit::karma::generate(ptr2, coordinate_scientific, f); - // Return end of the shorter string. - auto len2 = ptr2 - ptr; - if (ptr - buf > len2) { - // Move the shorter scientific form to the front. - memcpy(buf, ptr, len2); - ptr = buf + len2; - } - // Return pointer to the end. - return ptr; -#else - // Round-trippable float, shortest possible. - return buf + sprintf(buf, "%.9g", f); -#endif - }; - - char buf[256]; - unsigned int vertices_count = 0; - for (ModelVolume* volume : object.volumes) { - if (volume == nullptr) - continue; - - volumes_offsets.insert({ volume, Offsets(vertices_count) }); - - const indexed_triangle_set &its = volume->mesh().its; - if (its.vertices.empty()) { - add_error("Found invalid mesh"); - return false; - } - - vertices_count += (int)its.vertices.size(); - - const Transform3d& matrix = volume->get_matrix(); - - for (size_t i = 0; i < its.vertices.size(); ++i) { - Vec3f v = (matrix * its.vertices[i].cast()).cast(); - char *ptr = buf; - boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << VERTEX_TAG << " x=\""); - ptr = format_coordinate(v.x(), ptr); - boost::spirit::karma::generate(ptr, "\" y=\""); - ptr = format_coordinate(v.y(), ptr); - boost::spirit::karma::generate(ptr, "\" z=\""); - ptr = format_coordinate(v.z(), ptr); - boost::spirit::karma::generate(ptr, "\"/>\n"); - *ptr = '\0'; - output_buffer += buf; - if (! flush()) - return false; - } - } - - output_buffer += " \n <"; - output_buffer += TRIANGLES_TAG; - output_buffer += ">\n"; - - unsigned int triangles_count = 0; - for (ModelVolume* volume : object.volumes) { - if (volume == nullptr) - continue; - - bool is_left_handed = volume->is_left_handed(); - VolumeToOffsetsMap::iterator volume_it = volumes_offsets.find(volume); - assert(volume_it != volumes_offsets.end()); - - const indexed_triangle_set &its = volume->mesh().its; - - // updates triangle offsets - volume_it->second.first_triangle_id = triangles_count; - triangles_count += (int)its.indices.size(); - volume_it->second.last_triangle_id = triangles_count - 1; - - for (int i = 0; i < int(its.indices.size()); ++ i) { - { - const Vec3i &idx = its.indices[i]; - char *ptr = buf; - boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << TRIANGLE_TAG << - " v1=\"" << boost::spirit::int_ << - "\" v2=\"" << boost::spirit::int_ << - "\" v3=\"" << boost::spirit::int_ << "\"", - idx[is_left_handed ? 2 : 0] + volume_it->second.first_vertex_id, - idx[1] + volume_it->second.first_vertex_id, - idx[is_left_handed ? 0 : 2] + volume_it->second.first_vertex_id); - *ptr = '\0'; - output_buffer += buf; - } - - std::string custom_supports_data_string = volume->supported_facets.get_triangle_as_string(i); - if (! custom_supports_data_string.empty()) { - output_buffer += " "; - output_buffer += CUSTOM_SUPPORTS_ATTR; - output_buffer += "=\""; - output_buffer += custom_supports_data_string; - output_buffer += "\""; - } - - std::string custom_seam_data_string = volume->seam_facets.get_triangle_as_string(i); - if (! custom_seam_data_string.empty()) { - output_buffer += " "; - output_buffer += CUSTOM_SEAM_ATTR; - output_buffer += "=\""; - output_buffer += custom_seam_data_string; - output_buffer += "\""; - } - - std::string mmu_painting_data_string = volume->mmu_segmentation_facets.get_triangle_as_string(i); - if (! mmu_painting_data_string.empty()) { - output_buffer += " "; - output_buffer += MMU_SEGMENTATION_ATTR; - output_buffer += "=\""; - output_buffer += mmu_painting_data_string; - output_buffer += "\""; - } - - output_buffer += "/>\n"; - - if (! flush()) - return false; - } - } - - output_buffer += " \n \n"; - - // Force flush. - return flush(true); - } - - bool _3MF_Exporter::_add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) - { - // This happens for empty projects - if (build_items.size() == 0) { - add_error("No build item found"); - return true; - } - - stream << " <" << BUILD_TAG << ">\n"; - - for (const BuildItem& item : build_items) { - stream << " <" << ITEM_TAG << " " << OBJECTID_ATTR << "=\"" << item.id << "\" " << TRANSFORM_ATTR << "=\""; - for (unsigned c = 0; c < 4; ++c) { - for (unsigned r = 0; r < 3; ++r) { - stream << item.transform(r, c); - if (r != 2 || c != 3) - stream << " "; - } - } - stream << "\" " << PRINTABLE_ATTR << "=\"" << item.printable << "\"/>\n"; - } - - stream << " \n"; - - return true; - } - - bool _3MF_Exporter::_add_cut_information_file_to_archive(mz_zip_archive& archive, Model& model) - { - std::string out = ""; - pt::ptree tree; - - unsigned int object_cnt = 0; - for (const ModelObject* object : model.objects) { - object_cnt++; - pt::ptree& obj_tree = tree.add("objects.object", ""); - - obj_tree.put(".id", object_cnt); - - // Store info for cut_id - pt::ptree& cut_id_tree = obj_tree.add("cut_id", ""); - - // store cut_id atributes - cut_id_tree.put(".id", object->cut_id.id().id); - cut_id_tree.put(".check_sum", object->cut_id.check_sum()); - cut_id_tree.put(".connectors_cnt", object->cut_id.connectors_cnt()); - - int volume_idx = -1; - for (const ModelVolume* volume : object->volumes) { - ++volume_idx; - if (volume->is_cut_connector()) { - pt::ptree& connectors_tree = obj_tree.add("connectors.connector", ""); - connectors_tree.put(".volume_id", volume_idx); - connectors_tree.put(".type", int(volume->cut_info.connector_type)); - connectors_tree.put(".r_tolerance", volume->cut_info.radius_tolerance); - connectors_tree.put(".h_tolerance", volume->cut_info.height_tolerance); - } - } - } - - if (!tree.empty()) { - std::ostringstream oss; - pt::write_xml(oss, tree); - out = oss.str(); - - // Post processing("beautification") of the output string for a better preview - boost::replace_all(out, ">\n \n ", ">\n "); - boost::replace_all(out, ">\n ", ">\n "); - boost::replace_all(out, ">\n ", ">\n "); - boost::replace_all(out, ">", ">\n "); - // OR just - boost::replace_all(out, "><", ">\n<"); - } - - if (!out.empty()) { - if (!mz_zip_writer_add_mem(&archive, CUT_INFORMATION_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add cut information file to archive"); - return false; - } - } - - return true; - } - - bool _3MF_Exporter::_add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model) - { - assert(is_decimal_separator_point()); - std::string out = ""; - char buffer[1024]; - - unsigned int count = 0; - for (const ModelObject* object : model.objects) { - ++count; - const std::vector& layer_height_profile = object->layer_height_profile.get(); - if (layer_height_profile.size() >= 4 && layer_height_profile.size() % 2 == 0) { - sprintf(buffer, "object_id=%d|", count); - out += buffer; - - // Store the layer height profile as a single semicolon separated list. - for (size_t i = 0; i < layer_height_profile.size(); ++i) { - sprintf(buffer, (i == 0) ? "%f" : ";%f", layer_height_profile[i]); - out += buffer; - } - - out += "\n"; - } - } - - if (!out.empty()) { - if (!mz_zip_writer_add_mem(&archive, LAYER_HEIGHTS_PROFILE_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add layer heights profile file to archive"); - return false; - } - } - - return true; - } - - bool _3MF_Exporter::_add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model) - { - std::string out = ""; - pt::ptree tree; - - unsigned int object_cnt = 0; - for (const ModelObject* object : model.objects) { - object_cnt++; - const t_layer_config_ranges& ranges = object->layer_config_ranges; - if (!ranges.empty()) - { - pt::ptree& obj_tree = tree.add("objects.object",""); - - obj_tree.put(".id", object_cnt); - - // Store the layer config ranges. - for (const auto& range : ranges) { - pt::ptree& range_tree = obj_tree.add("range", ""); - - // store minX and maxZ - range_tree.put(".min_z", range.first.first); - range_tree.put(".max_z", range.first.second); - - // store range configuration - const ModelConfig& config = range.second; - for (const std::string& opt_key : config.keys()) { - pt::ptree& opt_tree = range_tree.add("option", config.opt_serialize(opt_key)); - opt_tree.put(".opt_key", opt_key); - } - } - } - } - - if (!tree.empty()) { - std::ostringstream oss; - pt::write_xml(oss, tree); - out = oss.str(); - - // Post processing("beautification") of the output string for a better preview - boost::replace_all(out, ">\n \n \n ", ">\n "); - boost::replace_all(out, ">", ">\n "); - // OR just - boost::replace_all(out, "><", ">\n<"); - } - - if (!out.empty()) { - if (!mz_zip_writer_add_mem(&archive, LAYER_CONFIG_RANGES_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add layer heights profile file to archive"); - return false; - } - } - - return true; - } - - bool _3MF_Exporter::_add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model) - { - assert(is_decimal_separator_point()); - std::string out = ""; - char buffer[1024]; - - unsigned int count = 0; - for (const ModelObject* object : model.objects) { - ++count; - const std::vector& sla_support_points = object->sla_support_points; - if (!sla_support_points.empty()) { - sprintf(buffer, "object_id=%d|", count); - out += buffer; - - // Store the layer height profile as a single space separated list. - for (size_t i = 0; i < sla_support_points.size(); ++i) { - sprintf(buffer, (i==0 ? "%f %f %f %f %f" : " %f %f %f %f %f"), sla_support_points[i].pos(0), sla_support_points[i].pos(1), sla_support_points[i].pos(2), sla_support_points[i].head_front_radius, (float)sla_support_points[i].is_new_island); - out += buffer; - } - out += "\n"; - } - } - - if (!out.empty()) { - // Adds version header at the beginning: - out = std::string("support_points_format_version=") + std::to_string(support_points_format_version) + std::string("\n") + out; - - if (!mz_zip_writer_add_mem(&archive, SLA_SUPPORT_POINTS_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add sla support points file to archive"); - return false; - } - } - return true; - } - - bool _3MF_Exporter::_add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model) - { - assert(is_decimal_separator_point()); - const char *const fmt = "object_id=%d|"; - std::string out; - - unsigned int count = 0; - for (const ModelObject* object : model.objects) { - ++count; - sla::DrainHoles drain_holes = object->sla_drain_holes; - - // The holes were placed 1mm above the mesh in the first implementation. - // This was a bad idea and the reference point was changed in 2.3 so - // to be on the mesh exactly. The elevated position is still saved - // in 3MFs for compatibility reasons. - for (sla::DrainHole& hole : drain_holes) { - hole.pos -= hole.normal.normalized(); - hole.height += 1.f; - } - - if (!drain_holes.empty()) { - out += string_printf(fmt, count); - - // Store the layer height profile as a single space separated list. - for (size_t i = 0; i < drain_holes.size(); ++i) - out += string_printf((i == 0 ? "%f %f %f %f %f %f %f %f" : " %f %f %f %f %f %f %f %f"), - drain_holes[i].pos(0), - drain_holes[i].pos(1), - drain_holes[i].pos(2), - drain_holes[i].normal(0), - drain_holes[i].normal(1), - drain_holes[i].normal(2), - drain_holes[i].radius, - drain_holes[i].height); - - out += "\n"; - } - } - - if (!out.empty()) { - // Adds version header at the beginning: - out = std::string("drain_holes_format_version=") + std::to_string(drain_holes_format_version) + std::string("\n") + out; - - if (!mz_zip_writer_add_mem(&archive, SLA_DRAIN_HOLES_FILE.c_str(), static_cast(out.data()), out.length(), mz_uint(MZ_DEFAULT_COMPRESSION))) { - add_error("Unable to add sla support points file to archive"); - return false; - } - } - return true; - } - - bool _3MF_Exporter::_add_print_config_file_to_archive(mz_zip_archive& archive, const DynamicPrintConfig &config) - { - assert(is_decimal_separator_point()); - char buffer[1024]; - sprintf(buffer, "; %s\n\n", header_slic3r_generated().c_str()); - std::string out = buffer; - - for (const std::string &key : config.keys()) - if (key != "compatible_printers") - out += "; " + key + " = " + config.opt_serialize(key) + "\n"; - - if (!out.empty()) { - if (!mz_zip_writer_add_mem(&archive, PRINT_CONFIG_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add print config file to archive"); - return false; - } - } - - return true; - } - - bool _3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, const IdToObjectDataMap &objects_data) - { - std::stringstream stream; - // Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back - // when loaded as accurately as possible. - stream << std::setprecision(std::numeric_limits::max_digits10); - stream << "\n"; - stream << "<" << CONFIG_TAG << ">\n"; - - for (const IdToObjectDataMap::value_type& obj_metadata : objects_data) { - const ModelObject* obj = obj_metadata.second.object; - if (obj != nullptr) { - // Output of instances count added because of github #3435, currently not used by PrusaSlicer - stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << obj_metadata.first << "\" " << INSTANCESCOUNT_ATTR << "=\"" << obj->instances.size() << "\">\n"; - - // stores object's name - if (!obj->name.empty()) - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << OBJECT_TYPE << "\" " << KEY_ATTR << "=\"name\" " << VALUE_ATTR << "=\"" << xml_escape(obj->name) << "\"/>\n"; - - // stores object's config data - for (const std::string& key : obj->config.keys()) { - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << OBJECT_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << obj->config.opt_serialize(key) << "\"/>\n"; - } - - for (const ModelVolume* volume : obj_metadata.second.object->volumes) { - if (volume != nullptr) { - const VolumeToOffsetsMap& offsets = obj_metadata.second.volumes_offsets; - VolumeToOffsetsMap::const_iterator it = offsets.find(volume); - if (it != offsets.end()) { - // stores volume's offsets - stream << " <" << VOLUME_TAG << " "; - stream << FIRST_TRIANGLE_ID_ATTR << "=\"" << it->second.first_triangle_id << "\" "; - stream << LAST_TRIANGLE_ID_ATTR << "=\"" << it->second.last_triangle_id << "\">\n"; - - // stores volume's name - if (!volume->name.empty()) - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << NAME_KEY << "\" " << VALUE_ATTR << "=\"" << xml_escape(volume->name) << "\"/>\n"; - - // stores volume's modifier field (legacy, to support old slicers) - if (volume->is_modifier()) - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MODIFIER_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; - // stores volume's type (overrides the modifier field above) - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << VOLUME_TYPE_KEY << "\" " << - VALUE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n"; - - // stores volume's local matrix - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MATRIX_KEY << "\" " << VALUE_ATTR << "=\""; - const Transform3d matrix = volume->get_matrix() * volume->source.transform.get_matrix(); - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - stream << matrix(r, c); - if (r != 3 || c != 3) - stream << " "; - } - } - stream << "\"/>\n"; - - // stores volume's source data - { - std::string input_file = xml_escape(m_fullpath_sources ? volume->source.input_file : boost::filesystem::path(volume->source.input_file).filename().string()); - std::string prefix = std::string(" <") + METADATA_TAG + " " + TYPE_ATTR + "=\"" + VOLUME_TYPE + "\" " + KEY_ATTR + "=\""; - if (! volume->source.input_file.empty()) { - stream << prefix << SOURCE_FILE_KEY << "\" " << VALUE_ATTR << "=\"" << input_file << "\"/>\n"; - stream << prefix << SOURCE_OBJECT_ID_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.object_idx << "\"/>\n"; - stream << prefix << SOURCE_VOLUME_ID_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.volume_idx << "\"/>\n"; - stream << prefix << SOURCE_OFFSET_X_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(0) << "\"/>\n"; - stream << prefix << SOURCE_OFFSET_Y_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(1) << "\"/>\n"; - stream << prefix << SOURCE_OFFSET_Z_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(2) << "\"/>\n"; - } - assert(! volume->source.is_converted_from_inches || ! volume->source.is_converted_from_meters); - if (volume->source.is_converted_from_inches) - stream << prefix << SOURCE_IN_INCHES_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; - else if (volume->source.is_converted_from_meters) - stream << prefix << SOURCE_IN_METERS_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; -#if ENABLE_RELOAD_FROM_DISK_REWORK - if (volume->source.is_from_builtin_objects) - stream << prefix << SOURCE_IS_BUILTIN_VOLUME_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; -#endif // ENABLE_RELOAD_FROM_DISK_REWORK - } - - // stores volume's config data - for (const std::string& key : volume->config.keys()) { - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; - } - - // stores mesh's statistics - const RepairedMeshErrors& stats = volume->mesh().stats().repaired_errors; - stream << " <" << MESH_TAG << " "; - stream << MESH_STAT_EDGES_FIXED << "=\"" << stats.edges_fixed << "\" "; - stream << MESH_STAT_DEGENERATED_FACETS << "=\"" << stats.degenerate_facets << "\" "; - stream << MESH_STAT_FACETS_REMOVED << "=\"" << stats.facets_removed << "\" "; - stream << MESH_STAT_FACETS_RESERVED << "=\"" << stats.facets_reversed << "\" "; - stream << MESH_STAT_BACKWARDS_EDGES << "=\"" << stats.backwards_edges << "\"/>\n"; - - stream << " \n"; - } - } - } - - stream << " \n"; - } - } - - stream << "\n"; - - std::string out = stream.str(); - - if (!mz_zip_writer_add_mem(&archive, MODEL_CONFIG_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add model config file to archive"); - return false; - } - - return true; - } - -bool _3MF_Exporter::_add_custom_gcode_per_print_z_file_to_archive( mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config) -{ - std::string out = ""; - - if (!model.custom_gcode_per_print_z.gcodes.empty()) { - pt::ptree tree; - pt::ptree& main_tree = tree.add("custom_gcodes_per_print_z", ""); - - for (const CustomGCode::Item& code : model.custom_gcode_per_print_z.gcodes) { - pt::ptree& code_tree = main_tree.add("code", ""); - - // store data of custom_gcode_per_print_z - code_tree.put(".print_z" , code.print_z ); - code_tree.put(".type" , static_cast(code.type)); - code_tree.put(".extruder" , code.extruder ); - code_tree.put(".color" , code.color ); - code_tree.put(".extra" , code.extra ); - - // add gcode field data for the old version of the PrusaSlicer - std::string gcode = code.type == CustomGCode::ColorChange ? config->opt_string("color_change_gcode") : - code.type == CustomGCode::PausePrint ? config->opt_string("pause_print_gcode") : - code.type == CustomGCode::Template ? config->opt_string("template_custom_gcode") : - code.type == CustomGCode::ToolChange ? "tool_change" : code.extra; - code_tree.put(".gcode" , gcode ); - } - - pt::ptree& mode_tree = main_tree.add("mode", ""); - // store mode of a custom_gcode_per_print_z - mode_tree.put(".value", model.custom_gcode_per_print_z.mode == CustomGCode::Mode::SingleExtruder ? CustomGCode::SingleExtruderMode : - model.custom_gcode_per_print_z.mode == CustomGCode::Mode::MultiAsSingle ? CustomGCode::MultiAsSingleMode : - CustomGCode::MultiExtruderMode); - - if (!tree.empty()) { - std::ostringstream oss; - boost::property_tree::write_xml(oss, tree); - out = oss.str(); - - // Post processing("beautification") of the output string - boost::replace_all(out, "><", ">\n<"); - } - } - - if (!out.empty()) { - if (!mz_zip_writer_add_mem(&archive, CUSTOM_GCODE_PER_PRINT_Z_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add custom Gcodes per print_z file to archive"); - return false; - } - } - - return true; -} - -// Perform conversions based on the config values available. -static void handle_legacy_project_loaded(unsigned int version_project_file, DynamicPrintConfig& config, const boost::optional& prusaslicer_generator_version) -{ - if (! config.has("brim_separation")) { - if (auto *opt_elephant_foot = config.option("elefant_foot_compensation", false); opt_elephant_foot) { - // Conversion from older PrusaSlicer which applied brim separation equal to elephant foot compensation. - auto *opt_brim_separation = config.option("brim_separation", true); - opt_brim_separation->value = opt_elephant_foot->value; - } - } - - // In PrusaSlicer 2.5.0-alpha2 and 2.5.0-alpha3, we introduce several parameters for Arachne that depend - // on nozzle size . Later we decided to make default values for those parameters computed automatically - // until the user changes them. - if (prusaslicer_generator_version && *prusaslicer_generator_version >= *Semver::parse("2.5.0-alpha2") && *prusaslicer_generator_version <= *Semver::parse("2.5.0-alpha3")) { - if (auto *opt_wall_transition_length = config.option("wall_transition_length", false); - opt_wall_transition_length && !opt_wall_transition_length->percent && opt_wall_transition_length->value == 0.4) { - opt_wall_transition_length->percent = true; - opt_wall_transition_length->value = 100; - } - - if (auto *opt_min_feature_size = config.option("min_feature_size", false); - opt_min_feature_size && !opt_min_feature_size->percent && opt_min_feature_size->value == 0.1) { - opt_min_feature_size->percent = true; - opt_min_feature_size->value = 25; - } - } -} - -bool is_project_3mf(const std::string& filename) -{ - mz_zip_archive archive; - mz_zip_zero_struct(&archive); - - if (!open_zip_reader(&archive, filename)) - return false; - - mz_uint num_entries = mz_zip_reader_get_num_files(&archive); - - // loop the entries to search for config - mz_zip_archive_file_stat stat; - bool config_found = false; - for (mz_uint i = 0; i < num_entries; ++i) { - if (mz_zip_reader_file_stat(&archive, i, &stat)) { - std::string name(stat.m_filename); - std::replace(name.begin(), name.end(), '\\', '/'); - - if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { - config_found = true; - break; - } - } - } - - close_zip_reader(&archive); - - return config_found; -} - -bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Model* model, bool check_version) -{ - if (path == nullptr || model == nullptr) - return false; - - // All import should use "C" locales for number formatting. - CNumericLocalesSetter locales_setter; - _3MF_Importer importer; - importer.load_model_from_file(path, *model, config, config_substitutions, check_version); - importer.log_errors(); - handle_legacy_project_loaded(importer.version(), config, importer.prusaslicer_generator_version()); - - return !model->objects.empty() || !config.empty(); -} - -bool store_3mf(const char* path, Model* model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64) -{ - // All export should use "C" locales for number formatting. - CNumericLocalesSetter locales_setter; - - if (path == nullptr || model == nullptr) - return false; - - _3MF_Exporter exporter; - bool res = exporter.save_model_to_file(path, *model, config, fullpath_sources, thumbnail_data, zip64); - if (!res) - exporter.log_errors(); - - return res; -} -} // namespace Slic3r +#include "../libslic3r.h" +#include "../Exception.hpp" +#include "../Model.hpp" +#include "../Utils.hpp" +#include "../LocalesUtils.hpp" +#include "../GCode.hpp" +#include "../Geometry.hpp" +#include "../GCode/ThumbnailData.hpp" +#include "../Semver.hpp" +#include "../Time.hpp" + +#include "../I18N.hpp" + +#include "3mf.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +namespace pt = boost::property_tree; + +#include +#include +#include "miniz_extension.hpp" + +#include "TextConfiguration.hpp" + +#include + +// Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, +// https://github.com/boostorg/spirit/pull/586 +// where the exported string is one digit shorter than it should be to guarantee lossless round trip. +// The code is left here for the ocasion boost guys improve. +#define EXPORT_3MF_USE_SPIRIT_KARMA_FP 0 + +// VERSION NUMBERS +// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them. +// 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files. +// 2 : Volumes' matrices and source data added to Metadata/Slic3r_PE_model.config file, meshes transformed back to their coordinate system on loading. +// WARNING !! -> the version number has been rolled back to 1 +// the next change should use 3 +const unsigned int VERSION_3MF = 1; +// Allow loading version 2 file as well. +const unsigned int VERSION_3MF_COMPATIBLE = 2; +const char* SLIC3RPE_3MF_VERSION = "slic3rpe:Version3mf"; // definition of the metadata name saved into .model file + +// Painting gizmos data version numbers +// 0 : 3MF files saved by older PrusaSlicer or the painting gizmo wasn't used. No version definition in them. +// 1 : Introduction of painting gizmos data versioning. No other changes in painting gizmos data. +const unsigned int FDM_SUPPORTS_PAINTING_VERSION = 1; +const unsigned int SEAM_PAINTING_VERSION = 1; +const unsigned int MM_PAINTING_VERSION = 1; + +const std::string SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION = "slic3rpe:FdmSupportsPaintingVersion"; +const std::string SLIC3RPE_SEAM_PAINTING_VERSION = "slic3rpe:SeamPaintingVersion"; +const std::string SLIC3RPE_MM_PAINTING_VERSION = "slic3rpe:MmPaintingVersion"; + +const std::string MODEL_FOLDER = "3D/"; +const std::string MODEL_EXTENSION = ".model"; +const std::string MODEL_FILE = "3D/3dmodel.model"; // << this is the only format of the string which works with CURA +const std::string CONTENT_TYPES_FILE = "[Content_Types].xml"; +const std::string RELATIONSHIPS_FILE = "_rels/.rels"; +const std::string THUMBNAIL_FILE = "Metadata/thumbnail.png"; +const std::string PRINT_CONFIG_FILE = "Metadata/Slic3r_PE.config"; +const std::string MODEL_CONFIG_FILE = "Metadata/Slic3r_PE_model.config"; +const std::string LAYER_HEIGHTS_PROFILE_FILE = "Metadata/Slic3r_PE_layer_heights_profile.txt"; +const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/Prusa_Slicer_layer_config_ranges.xml"; +const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt"; +const std::string SLA_DRAIN_HOLES_FILE = "Metadata/Slic3r_PE_sla_drain_holes.txt"; +const std::string CUSTOM_GCODE_PER_PRINT_Z_FILE = "Metadata/Prusa_Slicer_custom_gcode_per_print_z.xml"; +const std::string CUT_INFORMATION_FILE = "Metadata/Prusa_Slicer_cut_information.xml"; + +static constexpr const char* MODEL_TAG = "model"; +static constexpr const char* RESOURCES_TAG = "resources"; +static constexpr const char* OBJECT_TAG = "object"; +static constexpr const char* MESH_TAG = "mesh"; +static constexpr const char* VERTICES_TAG = "vertices"; +static constexpr const char* VERTEX_TAG = "vertex"; +static constexpr const char* TRIANGLES_TAG = "triangles"; +static constexpr const char* TRIANGLE_TAG = "triangle"; +static constexpr const char* COMPONENTS_TAG = "components"; +static constexpr const char* COMPONENT_TAG = "component"; +static constexpr const char* BUILD_TAG = "build"; +static constexpr const char* ITEM_TAG = "item"; +static constexpr const char* METADATA_TAG = "metadata"; + +static constexpr const char* CONFIG_TAG = "config"; +static constexpr const char* VOLUME_TAG = "volume"; + +static constexpr const char* UNIT_ATTR = "unit"; +static constexpr const char* NAME_ATTR = "name"; +static constexpr const char* TYPE_ATTR = "type"; +static constexpr const char* ID_ATTR = "id"; +static constexpr const char* X_ATTR = "x"; +static constexpr const char* Y_ATTR = "y"; +static constexpr const char* Z_ATTR = "z"; +static constexpr const char* V1_ATTR = "v1"; +static constexpr const char* V2_ATTR = "v2"; +static constexpr const char* V3_ATTR = "v3"; +static constexpr const char* OBJECTID_ATTR = "objectid"; +static constexpr const char* TRANSFORM_ATTR = "transform"; +static constexpr const char* PRINTABLE_ATTR = "printable"; +static constexpr const char* INSTANCESCOUNT_ATTR = "instances_count"; +static constexpr const char* CUSTOM_SUPPORTS_ATTR = "slic3rpe:custom_supports"; +static constexpr const char* CUSTOM_SEAM_ATTR = "slic3rpe:custom_seam"; +static constexpr const char* MMU_SEGMENTATION_ATTR = "slic3rpe:mmu_segmentation"; + +static constexpr const char* KEY_ATTR = "key"; +static constexpr const char* VALUE_ATTR = "value"; +static constexpr const char* FIRST_TRIANGLE_ID_ATTR = "firstid"; +static constexpr const char* LAST_TRIANGLE_ID_ATTR = "lastid"; + +static constexpr const char* OBJECT_TYPE = "object"; +static constexpr const char* VOLUME_TYPE = "volume"; + +static constexpr const char* NAME_KEY = "name"; +static constexpr const char* MODIFIER_KEY = "modifier"; +static constexpr const char* VOLUME_TYPE_KEY = "volume_type"; +static constexpr const char* MATRIX_KEY = "matrix"; +static constexpr const char* SOURCE_FILE_KEY = "source_file"; +static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id"; +static constexpr const char* SOURCE_VOLUME_ID_KEY = "source_volume_id"; +static constexpr const char* SOURCE_OFFSET_X_KEY = "source_offset_x"; +static constexpr const char* SOURCE_OFFSET_Y_KEY = "source_offset_y"; +static constexpr const char* SOURCE_OFFSET_Z_KEY = "source_offset_z"; +static constexpr const char* SOURCE_IN_INCHES_KEY = "source_in_inches"; +static constexpr const char* SOURCE_IN_METERS_KEY = "source_in_meters"; +#if ENABLE_RELOAD_FROM_DISK_REWORK +static constexpr const char* SOURCE_IS_BUILTIN_VOLUME_KEY = "source_is_builtin_volume"; +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + +static constexpr const char* MESH_STAT_EDGES_FIXED = "edges_fixed"; +static constexpr const char* MESH_STAT_DEGENERATED_FACETS = "degenerate_facets"; +static constexpr const char* MESH_STAT_FACETS_REMOVED = "facets_removed"; +static constexpr const char* MESH_STAT_FACETS_RESERVED = "facets_reversed"; +static constexpr const char* MESH_STAT_BACKWARDS_EDGES = "backwards_edges"; + +// Store / load of TextConfiguration +static constexpr const char *TEXT_TAG = "emboss"; +static constexpr const char *TEXT_DATA_ATTR = "text"; +// TextConfiguration::EmbossStyle +static constexpr const char *STYLE_NAME_ATTR = "style_name"; +static constexpr const char *FONT_DESCRIPTOR_ATTR = "font_descriptor"; +static constexpr const char *FONT_DESCRIPTOR_TYPE_ATTR = "font_descriptor_type"; + +// TextConfiguration::FontProperty +static constexpr const char *CHAR_GAP_ATTR = "char_gap"; +static constexpr const char *LINE_GAP_ATTR = "line_gap"; +static constexpr const char *LINE_HEIGHT_ATTR = "line_height"; +static constexpr const char *DEPTH_ATTR = "depth"; +static constexpr const char *USE_SURFACE_ATTR = "use_surface"; +static constexpr const char *BOLDNESS_ATTR = "boldness"; +static constexpr const char *SKEW_ATTR = "skew"; +static constexpr const char *DISTANCE_ATTR = "distance"; +static constexpr const char *ANGLE_ATTR = "angle"; +static constexpr const char *COLLECTION_NUMBER_ATTR = "collection"; + +static constexpr const char *FONT_FAMILY_ATTR = "family"; +static constexpr const char *FONT_FACE_NAME_ATTR = "face_name"; +static constexpr const char *FONT_STYLE_ATTR = "style"; +static constexpr const char *FONT_WEIGHT_ATTR = "weight"; + +const unsigned int VALID_OBJECT_TYPES_COUNT = 1; +const char* VALID_OBJECT_TYPES[] = +{ + "model" +}; + +const char* INVALID_OBJECT_TYPES[] = +{ + "solidsupport", + "support", + "surface", + "other" +}; + +class version_error : public Slic3r::FileIOError +{ +public: + version_error(const std::string& what_arg) : Slic3r::FileIOError(what_arg) {} + version_error(const char* what_arg) : Slic3r::FileIOError(what_arg) {} +}; + +const char* get_attribute_value_charptr(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + if ((attributes == nullptr) || (attributes_size == 0) || (attributes_size % 2 != 0) || (attribute_key == nullptr)) + return nullptr; + + for (unsigned int a = 0; a < attributes_size; a += 2) { + if (::strcmp(attributes[a], attribute_key) == 0) + return attributes[a + 1]; + } + + return nullptr; +} + +std::string get_attribute_value_string(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + const char* text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); + return (text != nullptr) ? text : ""; +} + +float get_attribute_value_float(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + float value = 0.0f; + if (const char *text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); text != nullptr) + fast_float::from_chars(text, text + strlen(text), value); + return value; +} + +int get_attribute_value_int(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + int value = 0; + if (const char *text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); text != nullptr) + boost::spirit::qi::parse(text, text + strlen(text), boost::spirit::qi::int_, value); + return value; +} + +bool get_attribute_value_bool(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + const char* text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); + return (text != nullptr) ? (bool)::atoi(text) : true; +} + +Slic3r::Transform3d get_transform_from_3mf_specs_string(const std::string& mat_str) +{ + // check: https://3mf.io/3d-manufacturing-format/ or https://github.com/3MFConsortium/spec_core/blob/master/3MF%20Core%20Specification.md + // to see how matrices are stored inside 3mf according to specifications + Slic3r::Transform3d ret = Slic3r::Transform3d::Identity(); + + if (mat_str.empty()) + // empty string means default identity matrix + return ret; + + std::vector mat_elements_str; + boost::split(mat_elements_str, mat_str, boost::is_any_of(" "), boost::token_compress_on); + + unsigned int size = (unsigned int)mat_elements_str.size(); + if (size != 12) + // invalid data, return identity matrix + return ret; + + unsigned int i = 0; + // matrices are stored into 3mf files as 4x3 + // we need to transpose them + for (unsigned int c = 0; c < 4; ++c) { + for (unsigned int r = 0; r < 3; ++r) { + ret(r, c) = ::atof(mat_elements_str[i++].c_str()); + } + } + return ret; +} + +float get_unit_factor(const std::string& unit) +{ + const char* text = unit.c_str(); + + if (::strcmp(text, "micron") == 0) + return 0.001f; + else if (::strcmp(text, "centimeter") == 0) + return 10.0f; + else if (::strcmp(text, "inch") == 0) + return 25.4f; + else if (::strcmp(text, "foot") == 0) + return 304.8f; + else if (::strcmp(text, "meter") == 0) + return 1000.0f; + else + // default "millimeters" (see specification) + return 1.0f; +} + +bool is_valid_object_type(const std::string& type) +{ + // if the type is empty defaults to "model" (see specification) + if (type.empty()) + return true; + + for (unsigned int i = 0; i < VALID_OBJECT_TYPES_COUNT; ++i) { + if (::strcmp(type.c_str(), VALID_OBJECT_TYPES[i]) == 0) + return true; + } + + return false; +} + +namespace Slic3r { + +//! macro used to mark string used at localization, +//! return same string +#define L(s) (s) +#define _(s) Slic3r::I18N::translate(s) + + // Base class with error messages management + class _3MF_Base + { + std::vector m_errors; + + protected: + void add_error(const std::string& error) { m_errors.push_back(error); } + void clear_errors() { m_errors.clear(); } + + public: + void log_errors() + { + for (const std::string& error : m_errors) + BOOST_LOG_TRIVIAL(error) << error; + } + }; + + class _3MF_Importer : public _3MF_Base + { + struct Component + { + int object_id; + Transform3d transform; + + explicit Component(int object_id) + : object_id(object_id) + , transform(Transform3d::Identity()) + { + } + + Component(int object_id, const Transform3d& transform) + : object_id(object_id) + , transform(transform) + { + } + }; + + typedef std::vector ComponentsList; + + struct Geometry + { + std::vector vertices; + std::vector triangles; + std::vector custom_supports; + std::vector custom_seam; + std::vector mmu_segmentation; + + bool empty() { return vertices.empty() || triangles.empty(); } + + void reset() { + vertices.clear(); + triangles.clear(); + custom_supports.clear(); + custom_seam.clear(); + mmu_segmentation.clear(); + } + }; + + struct CurrentObject + { + // ID of the object inside the 3MF file, 1 based. + int id; + // Index of the ModelObject in its respective Model, zero based. + int model_object_idx; + Geometry geometry; + ModelObject* object; + ComponentsList components; + + CurrentObject() { reset(); } + + void reset() { + id = -1; + model_object_idx = -1; + geometry.reset(); + object = nullptr; + components.clear(); + } + }; + + struct CurrentConfig + { + int object_id; + int volume_id; + }; + + struct Instance + { + ModelInstance* instance; + Transform3d transform; + + Instance(ModelInstance* instance, const Transform3d& transform) + : instance(instance) + , transform(transform) + { + } + }; + + struct Metadata + { + std::string key; + std::string value; + + Metadata(const std::string& key, const std::string& value) + : key(key) + , value(value) + { + } + }; + + typedef std::vector MetadataList; + + struct ObjectMetadata + { + struct VolumeMetadata + { + unsigned int first_triangle_id; + unsigned int last_triangle_id; + MetadataList metadata; + RepairedMeshErrors mesh_stats; + std::optional text_configuration; + VolumeMetadata(unsigned int first_triangle_id, unsigned int last_triangle_id) + : first_triangle_id(first_triangle_id) + , last_triangle_id(last_triangle_id) + { + } + }; + + typedef std::vector VolumeMetadataList; + + MetadataList metadata; + VolumeMetadataList volumes; + }; + + struct CutObjectInfo + { + struct Connector + { + int volume_id; + int type; + float r_tolerance; + float h_tolerance; + }; + CutObjectBase id; + std::vector connectors; + }; + + // Map from a 1 based 3MF object ID to a 0 based ModelObject index inside m_model->objects. + typedef std::map IdToModelObjectMap; + typedef std::map IdToAliasesMap; + typedef std::vector InstancesList; + typedef std::map IdToMetadataMap; + typedef std::map IdToGeometryMap; + typedef std::map> IdToLayerHeightsProfileMap; + typedef std::map IdToLayerConfigRangesMap; + typedef std::map IdToCutObjectInfoMap; + typedef std::map> IdToSlaSupportPointsMap; + typedef std::map> IdToSlaDrainHolesMap; + + // Version of the 3mf file + unsigned int m_version; + bool m_check_version; + + // Semantic version of PrusaSlicer, that generated this 3MF. + boost::optional m_prusaslicer_generator_version; + unsigned int m_fdm_supports_painting_version = 0; + unsigned int m_seam_painting_version = 0; + unsigned int m_mm_painting_version = 0; + + XML_Parser m_xml_parser; + // Error code returned by the application side of the parser. In that case the expat may not reliably deliver the error state + // after returning from XML_Parse() function, thus we keep the error state here. + bool m_parse_error { false }; + std::string m_parse_error_message; + Model* m_model; + float m_unit_factor; + CurrentObject m_curr_object; + IdToModelObjectMap m_objects; + IdToAliasesMap m_objects_aliases; + InstancesList m_instances; + IdToGeometryMap m_geometries; + CurrentConfig m_curr_config; + IdToMetadataMap m_objects_metadata; + IdToCutObjectInfoMap m_cut_object_infos; + IdToLayerHeightsProfileMap m_layer_heights_profiles; + IdToLayerConfigRangesMap m_layer_config_ranges; + IdToSlaSupportPointsMap m_sla_support_points; + IdToSlaDrainHolesMap m_sla_drain_holes; + std::string m_curr_metadata_name; + std::string m_curr_characters; + std::string m_name; + + public: + _3MF_Importer(); + ~_3MF_Importer(); + + bool load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version); + unsigned int version() const { return m_version; } + boost::optional prusaslicer_generator_version() const { return m_prusaslicer_generator_version; } + + private: + void _destroy_xml_parser(); + void _stop_xml_parser(const std::string& msg = std::string()); + + bool parse_error() const { return m_parse_error; } + const char* parse_error_message() const { + return m_parse_error ? + // The error was signalled by the user code, not the expat parser. + (m_parse_error_message.empty() ? "Invalid 3MF format" : m_parse_error_message.c_str()) : + // The error was signalled by the expat parser. + XML_ErrorString(XML_GetErrorCode(m_xml_parser)); + } + + bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); + bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); + void _extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions); + void _extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_sla_drain_holes_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + + void _extract_custom_gcode_per_print_z_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + + void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& subs_context, const std::string& archive_filename); + bool _extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); + + // handlers to parse the .model file + void _handle_start_model_xml_element(const char* name, const char** attributes); + void _handle_end_model_xml_element(const char* name); + void _handle_model_xml_characters(const XML_Char* s, int len); + + // handlers to parse the MODEL_CONFIG_FILE file + void _handle_start_config_xml_element(const char* name, const char** attributes); + void _handle_end_config_xml_element(const char* name); + + bool _handle_start_model(const char** attributes, unsigned int num_attributes); + bool _handle_end_model(); + + bool _handle_start_resources(const char** attributes, unsigned int num_attributes); + bool _handle_end_resources(); + + bool _handle_start_object(const char** attributes, unsigned int num_attributes); + bool _handle_end_object(); + + bool _handle_start_mesh(const char** attributes, unsigned int num_attributes); + bool _handle_end_mesh(); + + bool _handle_start_vertices(const char** attributes, unsigned int num_attributes); + bool _handle_end_vertices(); + + bool _handle_start_vertex(const char** attributes, unsigned int num_attributes); + bool _handle_end_vertex(); + + bool _handle_start_triangles(const char** attributes, unsigned int num_attributes); + bool _handle_end_triangles(); + + bool _handle_start_triangle(const char** attributes, unsigned int num_attributes); + bool _handle_end_triangle(); + + bool _handle_start_components(const char** attributes, unsigned int num_attributes); + bool _handle_end_components(); + + bool _handle_start_component(const char** attributes, unsigned int num_attributes); + bool _handle_end_component(); + + bool _handle_start_build(const char** attributes, unsigned int num_attributes); + bool _handle_end_build(); + + bool _handle_start_item(const char** attributes, unsigned int num_attributes); + bool _handle_end_item(); + + bool _handle_start_metadata(const char** attributes, unsigned int num_attributes); + bool _handle_end_metadata(); + + bool _handle_start_text_configuration(const char** attributes, unsigned int num_attributes); + + bool _create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter); + + void _apply_transform(ModelInstance& instance, const Transform3d& transform); + + bool _handle_start_config(const char** attributes, unsigned int num_attributes); + bool _handle_end_config(); + + bool _handle_start_config_object(const char** attributes, unsigned int num_attributes); + bool _handle_end_config_object(); + + bool _handle_start_config_volume(const char** attributes, unsigned int num_attributes); + bool _handle_start_config_volume_mesh(const char** attributes, unsigned int num_attributes); + bool _handle_end_config_volume(); + bool _handle_end_config_volume_mesh(); + + bool _handle_start_config_metadata(const char** attributes, unsigned int num_attributes); + bool _handle_end_config_metadata(); + + bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); + + // callbacks to parse the .model file + static void XMLCALL _handle_start_model_xml_element(void* userData, const char* name, const char** attributes); + static void XMLCALL _handle_end_model_xml_element(void* userData, const char* name); + static void XMLCALL _handle_model_xml_characters(void* userData, const XML_Char* s, int len); + + // callbacks to parse the MODEL_CONFIG_FILE file + static void XMLCALL _handle_start_config_xml_element(void* userData, const char* name, const char** attributes); + static void XMLCALL _handle_end_config_xml_element(void* userData, const char* name); + }; + + _3MF_Importer::_3MF_Importer() + : m_version(0) + , m_check_version(false) + , m_xml_parser(nullptr) + , m_model(nullptr) + , m_unit_factor(1.0f) + , m_curr_metadata_name("") + , m_curr_characters("") + , m_name("") + { + } + + _3MF_Importer::~_3MF_Importer() + { + _destroy_xml_parser(); + } + + bool _3MF_Importer::load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version) + { + m_version = 0; + m_fdm_supports_painting_version = 0; + m_seam_painting_version = 0; + m_mm_painting_version = 0; + m_check_version = check_version; + m_model = &model; + m_unit_factor = 1.0f; + m_curr_object.reset(); + m_objects.clear(); + m_objects_aliases.clear(); + m_instances.clear(); + m_geometries.clear(); + m_curr_config.object_id = -1; + m_curr_config.volume_id = -1; + m_objects_metadata.clear(); + m_layer_heights_profiles.clear(); + m_layer_config_ranges.clear(); + m_sla_support_points.clear(); + m_curr_metadata_name.clear(); + m_curr_characters.clear(); + clear_errors(); + + return _load_model_from_file(filename, model, config, config_substitutions); + } + + void _3MF_Importer::_destroy_xml_parser() + { + if (m_xml_parser != nullptr) { + XML_ParserFree(m_xml_parser); + m_xml_parser = nullptr; + } + } + + void _3MF_Importer::_stop_xml_parser(const std::string &msg) + { + assert(! m_parse_error); + assert(m_parse_error_message.empty()); + assert(m_xml_parser != nullptr); + m_parse_error = true; + m_parse_error_message = msg; + XML_StopParser(m_xml_parser, false); + } + + bool _3MF_Importer::_load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions) + { + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_reader(&archive, filename)) { + add_error("Unable to open the file"); + return false; + } + + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + + mz_zip_archive_file_stat stat; + + m_name = boost::filesystem::path(filename).stem().string(); + + // we first loop the entries to read from the archive the .model file only, in order to extract the version from it + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + std::string name(stat.m_filename); + std::replace(name.begin(), name.end(), '\\', '/'); + + if (boost::algorithm::istarts_with(name, MODEL_FOLDER) && boost::algorithm::iends_with(name, MODEL_EXTENSION)) { + try + { + // valid model name -> extract model + if (!_extract_model_from_archive(archive, stat)) { + close_zip_reader(&archive); + add_error("Archive does not contain a valid model"); + return false; + } + } + catch (const std::exception& e) + { + // ensure the zip archive is closed and rethrow the exception + close_zip_reader(&archive); + throw Slic3r::FileIOError(e.what()); + } + } + } + } + + // we then loop again the entries to read other files stored in the archive + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + std::string name(stat.m_filename); + std::replace(name.begin(), name.end(), '\\', '/'); + + if (boost::algorithm::iequals(name, LAYER_HEIGHTS_PROFILE_FILE)) { + // extract slic3r layer heights profile file + _extract_layer_heights_profile_config_from_archive(archive, stat); + } + else if (boost::algorithm::iequals(name, CUT_INFORMATION_FILE)) { + // extract slic3r layer config ranges file + _extract_cut_information_from_archive(archive, stat, config_substitutions); + } + else if (boost::algorithm::iequals(name, LAYER_CONFIG_RANGES_FILE)) { + // extract slic3r layer config ranges file + _extract_layer_config_ranges_from_archive(archive, stat, config_substitutions); + } + else if (boost::algorithm::iequals(name, SLA_SUPPORT_POINTS_FILE)) { + // extract sla support points file + _extract_sla_support_points_from_archive(archive, stat); + } + else if (boost::algorithm::iequals(name, SLA_DRAIN_HOLES_FILE)) { + // extract sla support points file + _extract_sla_drain_holes_from_archive(archive, stat); + } + else if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { + // extract slic3r print config file + _extract_print_config_from_archive(archive, stat, config, config_substitutions, filename); + } + else if (boost::algorithm::iequals(name, CUSTOM_GCODE_PER_PRINT_Z_FILE)) { + // extract slic3r layer config ranges file + _extract_custom_gcode_per_print_z_from_archive(archive, stat); + } + else if (boost::algorithm::iequals(name, MODEL_CONFIG_FILE)) { + // extract slic3r model config file + if (!_extract_model_config_from_archive(archive, stat, model)) { + close_zip_reader(&archive); + add_error("Archive does not contain a valid model config"); + return false; + } + } + } + } + + close_zip_reader(&archive); + + if (m_version == 0) { + // if the 3mf was not produced by PrusaSlicer and there is more than one instance, + // split the object in as many objects as instances + size_t curr_models_count = m_model->objects.size(); + size_t i = 0; + while (i < curr_models_count) { + ModelObject* model_object = m_model->objects[i]; + if (model_object->instances.size() > 1) { + // select the geometry associated with the original model object + const Geometry* geometry = nullptr; + for (const IdToModelObjectMap::value_type& object : m_objects) { + if (object.second == int(i)) { + IdToGeometryMap::const_iterator obj_geometry = m_geometries.find(object.first); + if (obj_geometry == m_geometries.end()) { + add_error("Unable to find object geometry"); + return false; + } + geometry = &obj_geometry->second; + break; + } + } + + if (geometry == nullptr) { + add_error("Unable to find object geometry"); + return false; + } + + // use the geometry to create the volumes in the new model objects + ObjectMetadata::VolumeMetadataList volumes(1, { 0, (unsigned int)geometry->triangles.size() - 1 }); + + // for each instance after the 1st, create a new model object containing only that instance + // and copy into it the geometry + while (model_object->instances.size() > 1) { + ModelObject* new_model_object = m_model->add_object(*model_object); + new_model_object->clear_instances(); + new_model_object->add_instance(*model_object->instances.back()); + model_object->delete_last_instance(); + if (!_generate_volumes(*new_model_object, *geometry, volumes, config_substitutions)) + return false; + } + } + ++i; + } + } + + for (const IdToModelObjectMap::value_type& object : m_objects) { + if (object.second >= int(m_model->objects.size())) { + add_error("Unable to find object"); + return false; + } + ModelObject* model_object = m_model->objects[object.second]; + IdToGeometryMap::const_iterator obj_geometry = m_geometries.find(object.first); + if (obj_geometry == m_geometries.end()) { + add_error("Unable to find object geometry"); + return false; + } + + // m_layer_heights_profiles are indexed by a 1 based model object index. + IdToLayerHeightsProfileMap::iterator obj_layer_heights_profile = m_layer_heights_profiles.find(object.second + 1); + if (obj_layer_heights_profile != m_layer_heights_profiles.end()) + model_object->layer_height_profile.set(std::move(obj_layer_heights_profile->second)); + + // m_layer_config_ranges are indexed by a 1 based model object index. + IdToLayerConfigRangesMap::iterator obj_layer_config_ranges = m_layer_config_ranges.find(object.second + 1); + if (obj_layer_config_ranges != m_layer_config_ranges.end()) + model_object->layer_config_ranges = std::move(obj_layer_config_ranges->second); + + // m_sla_support_points are indexed by a 1 based model object index. + IdToSlaSupportPointsMap::iterator obj_sla_support_points = m_sla_support_points.find(object.second + 1); + if (obj_sla_support_points != m_sla_support_points.end() && !obj_sla_support_points->second.empty()) { + model_object->sla_support_points = std::move(obj_sla_support_points->second); + model_object->sla_points_status = sla::PointsStatus::UserModified; + } + + IdToSlaDrainHolesMap::iterator obj_drain_holes = m_sla_drain_holes.find(object.second + 1); + if (obj_drain_holes != m_sla_drain_holes.end() && !obj_drain_holes->second.empty()) { + model_object->sla_drain_holes = std::move(obj_drain_holes->second); + } + + ObjectMetadata::VolumeMetadataList volumes; + ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr; + + IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first); + if (obj_metadata != m_objects_metadata.end()) { + // config data has been found, this model was saved using slic3r pe + + // apply object's name and config data + for (const Metadata& metadata : obj_metadata->second.metadata) { + if (metadata.key == "name") + model_object->name = metadata.value; + else + model_object->config.set_deserialize(metadata.key, metadata.value, config_substitutions); + } + + // select object's detected volumes + volumes_ptr = &obj_metadata->second.volumes; + } + else { + // config data not found, this model was not saved using slic3r pe + + // add the entire geometry as the single volume to generate + volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1); + + // select as volumes + volumes_ptr = &volumes; + } + + if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr, config_substitutions)) + return false; + + // Apply cut information for object if any was loaded + // m_cut_object_ids are indexed by a 1 based model object index. + IdToCutObjectInfoMap::iterator cut_object_info = m_cut_object_infos.find(object.second + 1); + if (cut_object_info != m_cut_object_infos.end()) { + model_object->cut_id = cut_object_info->second.id; + + for (auto connector : cut_object_info->second.connectors) { + assert(0 <= connector.volume_id && connector.volume_id <= int(model_object->volumes.size())); + model_object->volumes[connector.volume_id]->cut_info = + ModelVolume::CutInfo(CutConnectorType(connector.type), connector.r_tolerance, connector.h_tolerance, true); + } + } + } + + // If instances contain a single volume, the volume offset should be 0,0,0 + // This equals to say that instance world position and volume world position should match + // Correct all instances/volumes for which this does not hold + for (int obj_id = 0; obj_id < int(model.objects.size()); ++obj_id) { + ModelObject* o = model.objects[obj_id]; + if (o->volumes.size() == 1) { + ModelVolume* v = o->volumes.front(); + const Slic3r::Geometry::Transformation& first_inst_trafo = o->instances.front()->get_transformation(); + const Vec3d world_vol_offset = (first_inst_trafo * v->get_transformation()).get_offset(); + const Vec3d world_inst_offset = first_inst_trafo.get_offset(); + + if (!world_vol_offset.isApprox(world_inst_offset)) { + const Slic3r::Geometry::Transformation& vol_trafo = v->get_transformation(); + for (int inst_id = 0; inst_id < int(o->instances.size()); ++inst_id) { + ModelInstance* i = o->instances[inst_id]; + const Slic3r::Geometry::Transformation& inst_trafo = i->get_transformation(); + i->set_offset((inst_trafo * vol_trafo).get_offset()); + } + v->set_offset(Vec3d::Zero()); + } + } + } + +#if ENABLE_RELOAD_FROM_DISK_REWORK + for (int obj_id = 0; obj_id < int(model.objects.size()); ++obj_id) { + ModelObject* o = model.objects[obj_id]; + for (int vol_id = 0; vol_id < int(o->volumes.size()); ++vol_id) { + ModelVolume* v = o->volumes[vol_id]; + if (v->source.input_file.empty()) + v->source.input_file = v->name.empty() ? filename : v->name; + if (v->source.volume_idx == -1) + v->source.volume_idx = vol_id; + if (v->source.object_idx == -1) + v->source.object_idx = obj_id; + } + } +#else + int object_idx = 0; + for (ModelObject* o : model.objects) { + int volume_idx = 0; + for (ModelVolume* v : o->volumes) { + if (v->source.input_file.empty() && v->type() == ModelVolumeType::MODEL_PART) { + v->source.input_file = filename; + if (v->source.volume_idx == -1) + v->source.volume_idx = volume_idx; + if (v->source.object_idx == -1) + v->source.object_idx = object_idx; + } + ++volume_idx; + } + ++object_idx; + } +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + +// // fixes the min z of the model if negative +// model.adjust_min_z(); + + return true; + } + + bool _3MF_Importer::_extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) + { + if (stat.m_uncomp_size == 0) { + add_error("Found invalid size"); + return false; + } + + _destroy_xml_parser(); + + m_xml_parser = XML_ParserCreate(nullptr); + if (m_xml_parser == nullptr) { + add_error("Unable to create parser"); + return false; + } + + XML_SetUserData(m_xml_parser, (void*)this); + XML_SetElementHandler(m_xml_parser, _3MF_Importer::_handle_start_model_xml_element, _3MF_Importer::_handle_end_model_xml_element); + XML_SetCharacterDataHandler(m_xml_parser, _3MF_Importer::_handle_model_xml_characters); + + struct CallbackData + { + XML_Parser& parser; + _3MF_Importer& importer; + const mz_zip_archive_file_stat& stat; + + CallbackData(XML_Parser& parser, _3MF_Importer& importer, const mz_zip_archive_file_stat& stat) : parser(parser), importer(importer), stat(stat) {} + }; + + CallbackData data(m_xml_parser, *this, stat); + + mz_bool res = 0; + + try + { + res = mz_zip_reader_extract_file_to_callback(&archive, stat.m_filename, [](void* pOpaque, mz_uint64 file_ofs, const void* pBuf, size_t n)->size_t { + CallbackData* data = (CallbackData*)pOpaque; + if (!XML_Parse(data->parser, (const char*)pBuf, (int)n, (file_ofs + n == data->stat.m_uncomp_size) ? 1 : 0) || data->importer.parse_error()) { + char error_buf[1024]; + ::sprintf(error_buf, "Error (%s) while parsing '%s' at line %d", data->importer.parse_error_message(), data->stat.m_filename, (int)XML_GetCurrentLineNumber(data->parser)); + throw Slic3r::FileIOError(error_buf); + } + + return n; + }, &data, 0); + } + catch (const version_error& e) + { + // rethrow the exception + throw Slic3r::FileIOError(e.what()); + } + catch (std::exception& e) + { + add_error(e.what()); + return false; + } + + if (res == 0) { + add_error("Error while extracting model data from zip archive"); + return false; + } + + return true; + } + + void _3MF_Importer::_extract_cut_information_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) + { + if (stat.m_uncomp_size > 0) { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading cut information data to buffer"); + return; + } + + std::istringstream iss(buffer); // wrap returned xml to istringstream + pt::ptree objects_tree; + pt::read_xml(iss, objects_tree); + + for (const auto& object : objects_tree.get_child("objects")) { + pt::ptree object_tree = object.second; + int obj_idx = object_tree.get(".id", -1); + if (obj_idx <= 0) { + add_error("Found invalid object id"); + continue; + } + + IdToCutObjectInfoMap::iterator object_item = m_cut_object_infos.find(obj_idx); + if (object_item != m_cut_object_infos.end()) { + add_error("Found duplicated cut_object_id"); + continue; + } + + CutObjectBase cut_id; + std::vector connectors; + + for (const auto& obj_cut_info : object_tree) { + if (obj_cut_info.first == "cut_id") { + pt::ptree cut_id_tree = obj_cut_info.second; + cut_id = CutObjectBase(ObjectID( cut_id_tree.get(".id")), + cut_id_tree.get(".check_sum"), + cut_id_tree.get(".connectors_cnt")); + } + if (obj_cut_info.first == "connectors") { + pt::ptree cut_connectors_tree = obj_cut_info.second; + for (const auto& cut_connector : cut_connectors_tree) { + if (cut_connector.first != "connector") + continue; + pt::ptree connector_tree = cut_connector.second; + CutObjectInfo::Connector connector = {connector_tree.get(".volume_id"), + connector_tree.get(".type"), + connector_tree.get(".r_tolerance"), + connector_tree.get(".h_tolerance")}; + connectors.emplace_back(connector); + } + } + } + + CutObjectInfo cut_info {cut_id, connectors}; + m_cut_object_infos.insert({ obj_idx, cut_info }); + } + } + } + + void _3MF_Importer::_extract_print_config_from_archive( + mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, + DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, + const std::string& archive_filename) + { + if (stat.m_uncomp_size > 0) { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading config data to buffer"); + return; + } + //FIXME Loading a "will be one day a legacy format" of configuration in a form of a G-code comment. + // Each config line is prefixed with a semicolon (G-code comment), that is ugly. + + // Replacing the legacy function with load_from_ini_string_commented leads to issues when + // parsing 3MFs from before PrusaSlicer 2.0.0 (which can have duplicated entries in the INI. + // See https://github.com/prusa3d/PrusaSlicer/issues/7155. We'll revert it for now. + //config_substitutions.substitutions = config.load_from_ini_string_commented(std::move(buffer), config_substitutions.rule); + ConfigBase::load_from_gcode_string_legacy(config, buffer.data(), config_substitutions); + } + } + + void _3MF_Importer::_extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) + { + if (stat.m_uncomp_size > 0) { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading layer heights profile data to buffer"); + return; + } + + if (buffer.back() == '\n') + buffer.pop_back(); + + std::vector objects; + boost::split(objects, buffer, boost::is_any_of("\n"), boost::token_compress_off); + + for (const std::string& object : objects) { + std::vector object_data; + boost::split(object_data, object, boost::is_any_of("|"), boost::token_compress_off); + if (object_data.size() != 2) { + add_error("Error while reading object data"); + continue; + } + + std::vector object_data_id; + boost::split(object_data_id, object_data[0], boost::is_any_of("="), boost::token_compress_off); + if (object_data_id.size() != 2) { + add_error("Error while reading object id"); + continue; + } + + int object_id = std::atoi(object_data_id[1].c_str()); + if (object_id == 0) { + add_error("Found invalid object id"); + continue; + } + + IdToLayerHeightsProfileMap::iterator object_item = m_layer_heights_profiles.find(object_id); + if (object_item != m_layer_heights_profiles.end()) { + add_error("Found duplicated layer heights profile"); + continue; + } + + std::vector object_data_profile; + boost::split(object_data_profile, object_data[1], boost::is_any_of(";"), boost::token_compress_off); + if (object_data_profile.size() <= 4 || object_data_profile.size() % 2 != 0) { + add_error("Found invalid layer heights profile"); + continue; + } + + std::vector profile; + profile.reserve(object_data_profile.size()); + + for (const std::string& value : object_data_profile) { + profile.push_back((coordf_t)std::atof(value.c_str())); + } + + m_layer_heights_profiles.insert({ object_id, profile }); + } + } + } + + void _3MF_Importer::_extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) + { + if (stat.m_uncomp_size > 0) { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading layer config ranges data to buffer"); + return; + } + + std::istringstream iss(buffer); // wrap returned xml to istringstream + pt::ptree objects_tree; + pt::read_xml(iss, objects_tree); + + for (const auto& object : objects_tree.get_child("objects")) { + pt::ptree object_tree = object.second; + int obj_idx = object_tree.get(".id", -1); + if (obj_idx <= 0) { + add_error("Found invalid object id"); + continue; + } + + IdToLayerConfigRangesMap::iterator object_item = m_layer_config_ranges.find(obj_idx); + if (object_item != m_layer_config_ranges.end()) { + add_error("Found duplicated layer config range"); + continue; + } + + t_layer_config_ranges config_ranges; + + for (const auto& range : object_tree) { + if (range.first != "range") + continue; + pt::ptree range_tree = range.second; + double min_z = range_tree.get(".min_z"); + double max_z = range_tree.get(".max_z"); + + // get Z range information + DynamicPrintConfig config; + + for (const auto& option : range_tree) { + if (option.first != "option") + continue; + std::string opt_key = option.second.get(".opt_key"); + std::string value = option.second.data(); + config.set_deserialize(opt_key, value, config_substitutions); + } + + config_ranges[{ min_z, max_z }].assign_config(std::move(config)); + } + + if (!config_ranges.empty()) + m_layer_config_ranges.insert({ obj_idx, std::move(config_ranges) }); + } + } + } + + void _3MF_Importer::_extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) + { + if (stat.m_uncomp_size > 0) { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading sla support points data to buffer"); + return; + } + + if (buffer.back() == '\n') + buffer.pop_back(); + + std::vector objects; + boost::split(objects, buffer, boost::is_any_of("\n"), boost::token_compress_off); + + // Info on format versioning - see 3mf.hpp + int version = 0; + std::string key("support_points_format_version="); + if (!objects.empty() && objects[0].find(key) != std::string::npos) { + objects[0].erase(objects[0].begin(), objects[0].begin() + long(key.size())); // removes the string + version = std::stoi(objects[0]); + objects.erase(objects.begin()); // pop the header + } + + for (const std::string& object : objects) { + std::vector object_data; + boost::split(object_data, object, boost::is_any_of("|"), boost::token_compress_off); + + if (object_data.size() != 2) { + add_error("Error while reading object data"); + continue; + } + + std::vector object_data_id; + boost::split(object_data_id, object_data[0], boost::is_any_of("="), boost::token_compress_off); + if (object_data_id.size() != 2) { + add_error("Error while reading object id"); + continue; + } + + int object_id = std::atoi(object_data_id[1].c_str()); + if (object_id == 0) { + add_error("Found invalid object id"); + continue; + } + + IdToSlaSupportPointsMap::iterator object_item = m_sla_support_points.find(object_id); + if (object_item != m_sla_support_points.end()) { + add_error("Found duplicated SLA support points"); + continue; + } + + std::vector object_data_points; + boost::split(object_data_points, object_data[1], boost::is_any_of(" "), boost::token_compress_off); + + std::vector sla_support_points; + + if (version == 0) { + for (unsigned int i=0; i 0) { + std::string buffer(size_t(stat.m_uncomp_size), 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading sla support points data to buffer"); + return; + } + + if (buffer.back() == '\n') + buffer.pop_back(); + + std::vector objects; + boost::split(objects, buffer, boost::is_any_of("\n"), boost::token_compress_off); + + // Info on format versioning - see 3mf.hpp + int version = 0; + std::string key("drain_holes_format_version="); + if (!objects.empty() && objects[0].find(key) != std::string::npos) { + objects[0].erase(objects[0].begin(), objects[0].begin() + long(key.size())); // removes the string + version = std::stoi(objects[0]); + objects.erase(objects.begin()); // pop the header + } + + for (const std::string& object : objects) { + std::vector object_data; + boost::split(object_data, object, boost::is_any_of("|"), boost::token_compress_off); + + if (object_data.size() != 2) { + add_error("Error while reading object data"); + continue; + } + + std::vector object_data_id; + boost::split(object_data_id, object_data[0], boost::is_any_of("="), boost::token_compress_off); + if (object_data_id.size() != 2) { + add_error("Error while reading object id"); + continue; + } + + int object_id = std::atoi(object_data_id[1].c_str()); + if (object_id == 0) { + add_error("Found invalid object id"); + continue; + } + + IdToSlaDrainHolesMap::iterator object_item = m_sla_drain_holes.find(object_id); + if (object_item != m_sla_drain_holes.end()) { + add_error("Found duplicated SLA drain holes"); + continue; + } + + std::vector object_data_points; + boost::split(object_data_points, object_data[1], boost::is_any_of(" "), boost::token_compress_off); + + sla::DrainHoles sla_drain_holes; + + if (version == 1) { + for (unsigned int i=0; i 0) { + std::string buffer((size_t)stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) { + add_error("Error while reading custom Gcodes per height data to buffer"); + return; + } + + std::istringstream iss(buffer); // wrap returned xml to istringstream + pt::ptree main_tree; + pt::read_xml(iss, main_tree); + + if (main_tree.front().first != "custom_gcodes_per_print_z") + return; + pt::ptree code_tree = main_tree.front().second; + + m_model->custom_gcode_per_print_z.gcodes.clear(); + + for (const auto& code : code_tree) { + if (code.first == "mode") { + pt::ptree tree = code.second; + std::string mode = tree.get(".value"); + m_model->custom_gcode_per_print_z.mode = mode == CustomGCode::SingleExtruderMode ? CustomGCode::Mode::SingleExtruder : + mode == CustomGCode::MultiAsSingleMode ? CustomGCode::Mode::MultiAsSingle : + CustomGCode::Mode::MultiExtruder; + } + if (code.first != "code") + continue; + + pt::ptree tree = code.second; + double print_z = tree.get (".print_z" ); + int extruder = tree.get (".extruder"); + std::string color = tree.get (".color" ); + + CustomGCode::Type type; + std::string extra; + pt::ptree attr_tree = tree.find("")->second; + if (attr_tree.find("type") == attr_tree.not_found()) { + // It means that data was saved in old version (2.2.0 and older) of PrusaSlicer + // read old data ... + std::string gcode = tree.get (".gcode"); + // ... and interpret them to the new data + type = gcode == "M600" ? CustomGCode::ColorChange : + gcode == "M601" ? CustomGCode::PausePrint : + gcode == "tool_change" ? CustomGCode::ToolChange : CustomGCode::Custom; + extra = type == CustomGCode::PausePrint ? color : + type == CustomGCode::Custom ? gcode : ""; + } + else { + type = static_cast(tree.get(".type")); + extra = tree.get(".extra"); + } + m_model->custom_gcode_per_print_z.gcodes.push_back(CustomGCode::Item{print_z, type, extruder, color, extra}) ; + } + } + } + + void _3MF_Importer::_handle_start_model_xml_element(const char* name, const char** attributes) + { + if (m_xml_parser == nullptr) + return; + + bool res = true; + unsigned int num_attributes = (unsigned int)XML_GetSpecifiedAttributeCount(m_xml_parser); + + if (::strcmp(MODEL_TAG, name) == 0) + res = _handle_start_model(attributes, num_attributes); + else if (::strcmp(RESOURCES_TAG, name) == 0) + res = _handle_start_resources(attributes, num_attributes); + else if (::strcmp(OBJECT_TAG, name) == 0) + res = _handle_start_object(attributes, num_attributes); + else if (::strcmp(MESH_TAG, name) == 0) + res = _handle_start_mesh(attributes, num_attributes); + else if (::strcmp(VERTICES_TAG, name) == 0) + res = _handle_start_vertices(attributes, num_attributes); + else if (::strcmp(VERTEX_TAG, name) == 0) + res = _handle_start_vertex(attributes, num_attributes); + else if (::strcmp(TRIANGLES_TAG, name) == 0) + res = _handle_start_triangles(attributes, num_attributes); + else if (::strcmp(TRIANGLE_TAG, name) == 0) + res = _handle_start_triangle(attributes, num_attributes); + else if (::strcmp(COMPONENTS_TAG, name) == 0) + res = _handle_start_components(attributes, num_attributes); + else if (::strcmp(COMPONENT_TAG, name) == 0) + res = _handle_start_component(attributes, num_attributes); + else if (::strcmp(BUILD_TAG, name) == 0) + res = _handle_start_build(attributes, num_attributes); + else if (::strcmp(ITEM_TAG, name) == 0) + res = _handle_start_item(attributes, num_attributes); + else if (::strcmp(METADATA_TAG, name) == 0) + res = _handle_start_metadata(attributes, num_attributes); + + if (!res) + _stop_xml_parser(); + } + + void _3MF_Importer::_handle_end_model_xml_element(const char* name) + { + if (m_xml_parser == nullptr) + return; + + bool res = true; + + if (::strcmp(MODEL_TAG, name) == 0) + res = _handle_end_model(); + else if (::strcmp(RESOURCES_TAG, name) == 0) + res = _handle_end_resources(); + else if (::strcmp(OBJECT_TAG, name) == 0) + res = _handle_end_object(); + else if (::strcmp(MESH_TAG, name) == 0) + res = _handle_end_mesh(); + else if (::strcmp(VERTICES_TAG, name) == 0) + res = _handle_end_vertices(); + else if (::strcmp(VERTEX_TAG, name) == 0) + res = _handle_end_vertex(); + else if (::strcmp(TRIANGLES_TAG, name) == 0) + res = _handle_end_triangles(); + else if (::strcmp(TRIANGLE_TAG, name) == 0) + res = _handle_end_triangle(); + else if (::strcmp(COMPONENTS_TAG, name) == 0) + res = _handle_end_components(); + else if (::strcmp(COMPONENT_TAG, name) == 0) + res = _handle_end_component(); + else if (::strcmp(BUILD_TAG, name) == 0) + res = _handle_end_build(); + else if (::strcmp(ITEM_TAG, name) == 0) + res = _handle_end_item(); + else if (::strcmp(METADATA_TAG, name) == 0) + res = _handle_end_metadata(); + + if (!res) + _stop_xml_parser(); + } + + void _3MF_Importer::_handle_model_xml_characters(const XML_Char* s, int len) + { + m_curr_characters.append(s, len); + } + + void _3MF_Importer::_handle_start_config_xml_element(const char* name, const char** attributes) + { + if (m_xml_parser == nullptr) + return; + + bool res = true; + unsigned int num_attributes = (unsigned int)XML_GetSpecifiedAttributeCount(m_xml_parser); + + if (::strcmp(CONFIG_TAG, name) == 0) + res = _handle_start_config(attributes, num_attributes); + else if (::strcmp(OBJECT_TAG, name) == 0) + res = _handle_start_config_object(attributes, num_attributes); + else if (::strcmp(VOLUME_TAG, name) == 0) + res = _handle_start_config_volume(attributes, num_attributes); + else if (::strcmp(MESH_TAG, name) == 0) + res = _handle_start_config_volume_mesh(attributes, num_attributes); + else if (::strcmp(METADATA_TAG, name) == 0) + res = _handle_start_config_metadata(attributes, num_attributes); + else if (::strcmp(TEXT_TAG, name) == 0) + res = _handle_start_text_configuration(attributes, num_attributes); + + if (!res) + _stop_xml_parser(); + } + + void _3MF_Importer::_handle_end_config_xml_element(const char* name) + { + if (m_xml_parser == nullptr) + return; + + bool res = true; + + if (::strcmp(CONFIG_TAG, name) == 0) + res = _handle_end_config(); + else if (::strcmp(OBJECT_TAG, name) == 0) + res = _handle_end_config_object(); + else if (::strcmp(VOLUME_TAG, name) == 0) + res = _handle_end_config_volume(); + else if (::strcmp(MESH_TAG, name) == 0) + res = _handle_end_config_volume_mesh(); + else if (::strcmp(METADATA_TAG, name) == 0) + res = _handle_end_config_metadata(); + + if (!res) + _stop_xml_parser(); + } + + bool _3MF_Importer::_handle_start_model(const char** attributes, unsigned int num_attributes) + { + m_unit_factor = get_unit_factor(get_attribute_value_string(attributes, num_attributes, UNIT_ATTR)); + return true; + } + + bool _3MF_Importer::_handle_end_model() + { + // deletes all non-built or non-instanced objects + for (const IdToModelObjectMap::value_type& object : m_objects) { + if (object.second >= int(m_model->objects.size())) { + add_error("Unable to find object"); + return false; + } + ModelObject *model_object = m_model->objects[object.second]; + if (model_object != nullptr && model_object->instances.size() == 0) + m_model->delete_object(model_object); + } + + if (m_version == 0) { + // if the 3mf was not produced by PrusaSlicer and there is only one object, + // set the object name to match the filename + if (m_model->objects.size() == 1) + m_model->objects.front()->name = m_name; + } + + // applies instances' matrices + for (Instance& instance : m_instances) { + if (instance.instance != nullptr && instance.instance->get_object() != nullptr) + // apply the transform to the instance + _apply_transform(*instance.instance, instance.transform); + } + + return true; + } + + bool _3MF_Importer::_handle_start_resources(const char** attributes, unsigned int num_attributes) + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_end_resources() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_object(const char** attributes, unsigned int num_attributes) + { + // reset current data + m_curr_object.reset(); + + if (is_valid_object_type(get_attribute_value_string(attributes, num_attributes, TYPE_ATTR))) { + // create new object (it may be removed later if no instances are generated from it) + m_curr_object.model_object_idx = (int)m_model->objects.size(); + m_curr_object.object = m_model->add_object(); + if (m_curr_object.object == nullptr) { + add_error("Unable to create object"); + return false; + } + + // set object data + m_curr_object.object->name = get_attribute_value_string(attributes, num_attributes, NAME_ATTR); + if (m_curr_object.object->name.empty()) + m_curr_object.object->name = m_name + "_" + std::to_string(m_model->objects.size()); + + m_curr_object.id = get_attribute_value_int(attributes, num_attributes, ID_ATTR); + } + + return true; + } + + bool _3MF_Importer::_handle_end_object() + { + if (m_curr_object.object != nullptr) { + if (m_curr_object.geometry.empty()) { + // no geometry defined + // remove the object from the model + m_model->delete_object(m_curr_object.object); + + if (m_curr_object.components.empty()) { + // no components defined -> invalid object, delete it + IdToModelObjectMap::iterator object_item = m_objects.find(m_curr_object.id); + if (object_item != m_objects.end()) + m_objects.erase(object_item); + + IdToAliasesMap::iterator alias_item = m_objects_aliases.find(m_curr_object.id); + if (alias_item != m_objects_aliases.end()) + m_objects_aliases.erase(alias_item); + } + else + // adds components to aliases + m_objects_aliases.insert({ m_curr_object.id, m_curr_object.components }); + } + else { + // geometry defined, store it for later use + m_geometries.insert({ m_curr_object.id, std::move(m_curr_object.geometry) }); + + // stores the object for later use + if (m_objects.find(m_curr_object.id) == m_objects.end()) { + m_objects.insert({ m_curr_object.id, m_curr_object.model_object_idx }); + m_objects_aliases.insert({ m_curr_object.id, { 1, Component(m_curr_object.id) } }); // aliases itself + } + else { + add_error("Found object with duplicate id"); + return false; + } + } + } + + return true; + } + + bool _3MF_Importer::_handle_start_mesh(const char** attributes, unsigned int num_attributes) + { + // reset current geometry + m_curr_object.geometry.reset(); + return true; + } + + bool _3MF_Importer::_handle_end_mesh() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_vertices(const char** attributes, unsigned int num_attributes) + { + // reset current vertices + m_curr_object.geometry.vertices.clear(); + return true; + } + + bool _3MF_Importer::_handle_end_vertices() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_vertex(const char** attributes, unsigned int num_attributes) + { + // appends the vertex coordinates + // missing values are set equal to ZERO + m_curr_object.geometry.vertices.emplace_back( + m_unit_factor * get_attribute_value_float(attributes, num_attributes, X_ATTR), + m_unit_factor * get_attribute_value_float(attributes, num_attributes, Y_ATTR), + m_unit_factor * get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + return true; + } + + bool _3MF_Importer::_handle_end_vertex() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_triangles(const char** attributes, unsigned int num_attributes) + { + // reset current triangles + m_curr_object.geometry.triangles.clear(); + return true; + } + + bool _3MF_Importer::_handle_end_triangles() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_triangle(const char** attributes, unsigned int num_attributes) + { + // we are ignoring the following attributes: + // p1 + // p2 + // p3 + // pid + // see specifications + + // appends the triangle's vertices indices + // missing values are set equal to ZERO + m_curr_object.geometry.triangles.emplace_back( + get_attribute_value_int(attributes, num_attributes, V1_ATTR), + get_attribute_value_int(attributes, num_attributes, V2_ATTR), + get_attribute_value_int(attributes, num_attributes, V3_ATTR)); + + m_curr_object.geometry.custom_supports.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SUPPORTS_ATTR)); + m_curr_object.geometry.custom_seam.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); + m_curr_object.geometry.mmu_segmentation.push_back(get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); + return true; + } + + bool _3MF_Importer::_handle_end_triangle() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_components(const char** attributes, unsigned int num_attributes) + { + // reset current components + m_curr_object.components.clear(); + return true; + } + + bool _3MF_Importer::_handle_end_components() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_component(const char** attributes, unsigned int num_attributes) + { + int object_id = get_attribute_value_int(attributes, num_attributes, OBJECTID_ATTR); + Transform3d transform = get_transform_from_3mf_specs_string(get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); + + IdToModelObjectMap::iterator object_item = m_objects.find(object_id); + if (object_item == m_objects.end()) { + IdToAliasesMap::iterator alias_item = m_objects_aliases.find(object_id); + if (alias_item == m_objects_aliases.end()) { + add_error("Found component with invalid object id"); + return false; + } + } + + m_curr_object.components.emplace_back(object_id, transform); + + return true; + } + + bool _3MF_Importer::_handle_end_component() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_build(const char** attributes, unsigned int num_attributes) + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_end_build() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_item(const char** attributes, unsigned int num_attributes) + { + // we are ignoring the following attributes + // thumbnail + // partnumber + // pid + // pindex + // see specifications + + int object_id = get_attribute_value_int(attributes, num_attributes, OBJECTID_ATTR); + Transform3d transform = get_transform_from_3mf_specs_string(get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); + int printable = get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR); + + return _create_object_instance(object_id, transform, printable, 1); + } + + bool _3MF_Importer::_handle_end_item() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_metadata(const char** attributes, unsigned int num_attributes) + { + m_curr_characters.clear(); + + std::string name = get_attribute_value_string(attributes, num_attributes, NAME_ATTR); + if (!name.empty()) + m_curr_metadata_name = name; + + return true; + } + + inline static void check_painting_version(unsigned int loaded_version, unsigned int highest_supported_version, const std::string &error_msg) + { + if (loaded_version > highest_supported_version) + throw version_error(error_msg); + } + + bool _3MF_Importer::_handle_end_metadata() + { + if (m_curr_metadata_name == SLIC3RPE_3MF_VERSION) { + m_version = (unsigned int)atoi(m_curr_characters.c_str()); + if (m_check_version && (m_version > VERSION_3MF_COMPATIBLE)) { + // std::string msg = _(L("The selected 3mf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible.")); + // throw version_error(msg.c_str()); + const std::string msg = (boost::format(_(L("The selected 3mf file has been saved with a newer version of %1% and is not compatible."))) % std::string(SLIC3R_APP_NAME)).str(); + throw version_error(msg); + } + } else if (m_curr_metadata_name == "Application") { + // Generator application of the 3MF. + // SLIC3R_APP_KEY - SLIC3R_VERSION + if (boost::starts_with(m_curr_characters, "PrusaSlicer-")) + m_prusaslicer_generator_version = Semver::parse(m_curr_characters.substr(12)); + } else if (m_curr_metadata_name == SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION) { + m_fdm_supports_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); + check_painting_version(m_fdm_supports_painting_version, FDM_SUPPORTS_PAINTING_VERSION, + _(L("The selected 3MF contains FDM supports painted object using a newer version of PrusaSlicer and is not compatible."))); + } else if (m_curr_metadata_name == SLIC3RPE_SEAM_PAINTING_VERSION) { + m_seam_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); + check_painting_version(m_seam_painting_version, SEAM_PAINTING_VERSION, + _(L("The selected 3MF contains seam painted object using a newer version of PrusaSlicer and is not compatible."))); + } else if (m_curr_metadata_name == SLIC3RPE_MM_PAINTING_VERSION) { + m_mm_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); + check_painting_version(m_mm_painting_version, MM_PAINTING_VERSION, + _(L("The selected 3MF contains multi-material painted object using a newer version of PrusaSlicer and is not compatible."))); + } + + return true; + } + + struct TextConfigurationSerialization + { + public: + TextConfigurationSerialization() = delete; + + static const boost::bimap type_to_name; + + static EmbossStyle::Type get_type(std::string_view type) { + const auto& to_type = TextConfigurationSerialization::type_to_name.right; + auto type_item = to_type.find(type); + assert(type_item != to_type.end()); + if (type_item == to_type.end()) return EmbossStyle::Type::undefined; + return type_item->second; + } + + static std::string_view get_name(EmbossStyle::Type type) { + const auto& to_name = TextConfigurationSerialization::type_to_name.left; + auto type_name = to_name.find(type); + assert(type_name != to_name.end()); + if (type_name == to_name.end()) return "unknown type"; + return type_name->second; + } + + static void to_xml(std::stringstream &stream, const TextConfiguration &tc); + static void create_fix_and_store(std::stringstream &stream, TextConfiguration tc, const ModelVolume& volume); + static std::optional read(const char **attributes, unsigned int num_attributes); + }; + + bool _3MF_Importer::_handle_start_text_configuration(const char **attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Cannot assign volume mesh to a valid object"); + return false; + } + if (object->second.volumes.empty()) { + add_error("Cannot assign mesh to a valid volume"); + return false; + } + ObjectMetadata::VolumeMetadata& volume = object->second.volumes.back(); + volume.text_configuration = TextConfigurationSerialization::read(attributes, num_attributes); + return volume.text_configuration.has_value(); + } + + bool _3MF_Importer::_create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter) + { + static const unsigned int MAX_RECURSIONS = 10; + + // escape from circular aliasing + if (recur_counter > MAX_RECURSIONS) { + add_error("Too many recursions"); + return false; + } + + IdToAliasesMap::iterator it = m_objects_aliases.find(object_id); + if (it == m_objects_aliases.end()) { + add_error("Found item with invalid object id"); + return false; + } + + if (it->second.size() == 1 && it->second[0].object_id == object_id) { + // aliasing to itself + + IdToModelObjectMap::iterator object_item = m_objects.find(object_id); + if (object_item == m_objects.end() || object_item->second == -1) { + add_error("Found invalid object"); + return false; + } + else { + ModelInstance* instance = m_model->objects[object_item->second]->add_instance(); + if (instance == nullptr) { + add_error("Unable to add object instance"); + return false; + } + instance->printable = printable; + + m_instances.emplace_back(instance, transform); + } + } + else { + // recursively process nested components + for (const Component& component : it->second) { + if (!_create_object_instance(component.object_id, transform * component.transform, printable, recur_counter + 1)) + return false; + } + } + + return true; + } + + void _3MF_Importer::_apply_transform(ModelInstance& instance, const Transform3d& transform) + { + Slic3r::Geometry::Transformation t(transform); + // invalid scale value, return + if (!t.get_scaling_factor().all()) + return; + + instance.set_transformation(t); + } + + bool _3MF_Importer::_handle_start_config(const char** attributes, unsigned int num_attributes) + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_end_config() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_config_object(const char** attributes, unsigned int num_attributes) + { + int object_id = get_attribute_value_int(attributes, num_attributes, ID_ATTR); + IdToMetadataMap::iterator object_item = m_objects_metadata.find(object_id); + if (object_item != m_objects_metadata.end()) { + add_error("Found duplicated object id"); + return false; + } + + // Added because of github #3435, currently not used by PrusaSlicer + // int instances_count_id = get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR); + + m_objects_metadata.insert({ object_id, ObjectMetadata() }); + m_curr_config.object_id = object_id; + return true; + } + + bool _3MF_Importer::_handle_end_config_object() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_config_volume(const char** attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Cannot assign volume to a valid object"); + return false; + } + + m_curr_config.volume_id = (int)object->second.volumes.size(); + + unsigned int first_triangle_id = (unsigned int)get_attribute_value_int(attributes, num_attributes, FIRST_TRIANGLE_ID_ATTR); + unsigned int last_triangle_id = (unsigned int)get_attribute_value_int(attributes, num_attributes, LAST_TRIANGLE_ID_ATTR); + + object->second.volumes.emplace_back(first_triangle_id, last_triangle_id); + return true; + } + + bool _3MF_Importer::_handle_start_config_volume_mesh(const char** attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Cannot assign volume mesh to a valid object"); + return false; + } + if (object->second.volumes.empty()) { + add_error("Cannot assign mesh to a valid volume"); + return false; + } + + ObjectMetadata::VolumeMetadata& volume = object->second.volumes.back(); + + int edges_fixed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_EDGES_FIXED ); + int degenerate_facets = get_attribute_value_int(attributes, num_attributes, MESH_STAT_DEGENERATED_FACETS); + int facets_removed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_FACETS_REMOVED ); + int facets_reversed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_FACETS_RESERVED ); + int backwards_edges = get_attribute_value_int(attributes, num_attributes, MESH_STAT_BACKWARDS_EDGES ); + + volume.mesh_stats = { edges_fixed, degenerate_facets, facets_removed, facets_reversed, backwards_edges }; + + return true; + } + + bool _3MF_Importer::_handle_end_config_volume() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_end_config_volume_mesh() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_config_metadata(const char** attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Cannot assign metadata to valid object id"); + return false; + } + + std::string type = get_attribute_value_string(attributes, num_attributes, TYPE_ATTR); + std::string key = get_attribute_value_string(attributes, num_attributes, KEY_ATTR); + std::string value = get_attribute_value_string(attributes, num_attributes, VALUE_ATTR); + + if (type == OBJECT_TYPE) + object->second.metadata.emplace_back(key, value); + else if (type == VOLUME_TYPE) { + if (size_t(m_curr_config.volume_id) < object->second.volumes.size()) + object->second.volumes[m_curr_config.volume_id].metadata.emplace_back(key, value); + } + else { + add_error("Found invalid metadata type"); + return false; + } + + return true; + } + + bool _3MF_Importer::_handle_end_config_metadata() + { + // do nothing + return true; + } + + bool _3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions) + { + if (!object.volumes.empty()) { + add_error("Found invalid volumes count"); + return false; + } + + unsigned int geo_tri_count = (unsigned int)geometry.triangles.size(); + unsigned int renamed_volumes_count = 0; + + for (const ObjectMetadata::VolumeMetadata& volume_data : volumes) { + if (geo_tri_count <= volume_data.first_triangle_id || geo_tri_count <= volume_data.last_triangle_id || volume_data.last_triangle_id < volume_data.first_triangle_id) { + add_error("Found invalid triangle id"); + return false; + } + + Transform3d volume_matrix_to_object = Transform3d::Identity(); + bool has_transform = false; + // extract the volume transformation from the volume's metadata, if present + for (const Metadata& metadata : volume_data.metadata) { + if (metadata.key == MATRIX_KEY) { + volume_matrix_to_object = Slic3r::Geometry::transform3d_from_string(metadata.value); + has_transform = ! volume_matrix_to_object.isApprox(Transform3d::Identity(), 1e-10); + break; + } + } + + // splits volume out of imported geometry + indexed_triangle_set its; + its.indices.assign(geometry.triangles.begin() + volume_data.first_triangle_id, geometry.triangles.begin() + volume_data.last_triangle_id + 1); + const size_t triangles_count = its.indices.size(); + if (triangles_count == 0) { + add_error("An empty triangle mesh found"); + return false; + } + + { + int min_id = its.indices.front()[0]; + int max_id = min_id; + for (const Vec3i& face : its.indices) { + for (const int tri_id : face) { + if (tri_id < 0 || tri_id >= int(geometry.vertices.size())) { + add_error("Found invalid vertex id"); + return false; + } + min_id = std::min(min_id, tri_id); + max_id = std::max(max_id, tri_id); + } + } + its.vertices.assign(geometry.vertices.begin() + min_id, geometry.vertices.begin() + max_id + 1); + + // rebase indices to the current vertices list + for (Vec3i& face : its.indices) + for (int& tri_id : face) + tri_id -= min_id; + } + + if (m_prusaslicer_generator_version && + *m_prusaslicer_generator_version >= *Semver::parse("2.4.0-alpha1") && + *m_prusaslicer_generator_version < *Semver::parse("2.4.0-alpha3")) + // PrusaSlicer 2.4.0-alpha2 contained a bug, where all vertices of a single object were saved for each volume the object contained. + // Remove the vertices, that are not referenced by any face. + its_compactify_vertices(its, true); + + TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats); + + if (m_version == 0) { + // if the 3mf was not produced by PrusaSlicer and there is only one instance, + // bake the transformation into the geometry to allow the reload from disk command + // to work properly + if (object.instances.size() == 1) { + triangle_mesh.transform(object.instances.front()->get_transformation().get_matrix(), false); + object.instances.front()->set_transformation(Slic3r::Geometry::Transformation()); + //FIXME do the mesh fixing? + } + } + if (triangle_mesh.volume() < 0) + triangle_mesh.flip_triangles(); + + ModelVolume* volume = object.add_volume(std::move(triangle_mesh)); + // stores the volume matrix taken from the metadata, if present + if (has_transform) + volume->source.transform = Slic3r::Geometry::Transformation(volume_matrix_to_object); + + // recreate custom supports, seam and mmu segmentation from previously loaded attribute + volume->supported_facets.reserve(triangles_count); + volume->seam_facets.reserve(triangles_count); + volume->mmu_segmentation_facets.reserve(triangles_count); + for (size_t i=0; isupported_facets.set_triangle_from_string(i, geometry.custom_supports[index]); + if (! geometry.custom_seam[index].empty()) + volume->seam_facets.set_triangle_from_string(i, geometry.custom_seam[index]); + if (! geometry.mmu_segmentation[index].empty()) + volume->mmu_segmentation_facets.set_triangle_from_string(i, geometry.mmu_segmentation[index]); + } + volume->supported_facets.shrink_to_fit(); + volume->seam_facets.shrink_to_fit(); + volume->mmu_segmentation_facets.shrink_to_fit(); + auto &tc = volume_data.text_configuration; + if (tc.has_value()) { + volume->text_configuration = std::move(tc); + + //// Transformation before store to 3mf + //const Transform3d &pre_trmat = *tc->fix_3mf_tr; + //// Cannot use source tranformation + //// When store transformed againg to 3mf it is not modified !!! + //// const Transform3d &pre_trmat = volume->source.transform.get_matrix(); + + //// create fix transformation + //assert(tc->fix_3mf_tr.has_value()); + //volume->text_configuration->fix_3mf_tr = + // pre_trmat.inverse() * + // volume->get_transformation().get_matrix(); + } + + // apply the remaining volume's metadata + for (const Metadata& metadata : volume_data.metadata) { + if (metadata.key == NAME_KEY) + volume->name = metadata.value; + else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1")) + volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); + else if (metadata.key == VOLUME_TYPE_KEY) + volume->set_type(ModelVolume::type_from_string(metadata.value)); + else if (metadata.key == SOURCE_FILE_KEY) + volume->source.input_file = metadata.value; + else if (metadata.key == SOURCE_OBJECT_ID_KEY) + volume->source.object_idx = ::atoi(metadata.value.c_str()); + else if (metadata.key == SOURCE_VOLUME_ID_KEY) + volume->source.volume_idx = ::atoi(metadata.value.c_str()); + else if (metadata.key == SOURCE_OFFSET_X_KEY) + volume->source.mesh_offset.x() = ::atof(metadata.value.c_str()); + else if (metadata.key == SOURCE_OFFSET_Y_KEY) + volume->source.mesh_offset.y() = ::atof(metadata.value.c_str()); + else if (metadata.key == SOURCE_OFFSET_Z_KEY) + volume->source.mesh_offset.z() = ::atof(metadata.value.c_str()); + else if (metadata.key == SOURCE_IN_INCHES_KEY) + volume->source.is_converted_from_inches = metadata.value == "1"; + else if (metadata.key == SOURCE_IN_METERS_KEY) + volume->source.is_converted_from_meters = metadata.value == "1"; +#if ENABLE_RELOAD_FROM_DISK_REWORK + else if (metadata.key == SOURCE_IS_BUILTIN_VOLUME_KEY) + volume->source.is_from_builtin_objects = metadata.value == "1"; +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + else + volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); + } + + // this may happen for 3mf saved by 3rd part softwares + if (volume->name.empty()) { + volume->name = object.name; + if (renamed_volumes_count > 0) + volume->name += "_" + std::to_string(renamed_volumes_count + 1); + ++renamed_volumes_count; + } + } + + return true; + } + + void XMLCALL _3MF_Importer::_handle_start_model_xml_element(void* userData, const char* name, const char** attributes) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_start_model_xml_element(name, attributes); + } + + void XMLCALL _3MF_Importer::_handle_end_model_xml_element(void* userData, const char* name) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_end_model_xml_element(name); + } + + void XMLCALL _3MF_Importer::_handle_model_xml_characters(void* userData, const XML_Char* s, int len) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_model_xml_characters(s, len); + } + + void XMLCALL _3MF_Importer::_handle_start_config_xml_element(void* userData, const char* name, const char** attributes) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_start_config_xml_element(name, attributes); + } + + void XMLCALL _3MF_Importer::_handle_end_config_xml_element(void* userData, const char* name) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_end_config_xml_element(name); + } + + class _3MF_Exporter : public _3MF_Base + { + struct BuildItem + { + unsigned int id; + Transform3d transform; + bool printable; + + BuildItem(unsigned int id, const Transform3d& transform, const bool printable) + : id(id) + , transform(transform) + , printable(printable) + { + } + }; + + struct Offsets + { + unsigned int first_vertex_id; + unsigned int first_triangle_id; + unsigned int last_triangle_id; + + Offsets(unsigned int first_vertex_id) + : first_vertex_id(first_vertex_id) + , first_triangle_id(-1) + , last_triangle_id(-1) + { + } + }; + + typedef std::map VolumeToOffsetsMap; + + struct ObjectData + { + ModelObject* object; + VolumeToOffsetsMap volumes_offsets; + + explicit ObjectData(ModelObject* object) + : object(object) + { + } + }; + + typedef std::vector BuildItemsList; + typedef std::map IdToObjectDataMap; + + bool m_fullpath_sources{ true }; + bool m_zip64 { true }; + + public: + bool save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64); + static void add_transformation(std::stringstream &stream, const Transform3d &tr); + private: + bool _save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data); + bool _add_content_types_file_to_archive(mz_zip_archive& archive); + bool _add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data); + bool _add_relationships_file_to_archive(mz_zip_archive& archive); + bool _add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data); + bool _add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets); + bool _add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets); + bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items); + bool _add_cut_information_file_to_archive(mz_zip_archive& archive, Model& model); + bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model); + bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model); + bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model); + bool _add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model); + bool _add_print_config_file_to_archive(mz_zip_archive& archive, const DynamicPrintConfig &config); + bool _add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, const IdToObjectDataMap &objects_data); + bool _add_custom_gcode_per_print_z_file_to_archive(mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config); + }; + + bool _3MF_Exporter::save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64) + { + clear_errors(); + m_fullpath_sources = fullpath_sources; + m_zip64 = zip64; + return _save_model_to_file(filename, model, config, thumbnail_data); + } + + bool _3MF_Exporter::_save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data) + { + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_writer(&archive, filename)) { + add_error("Unable to open the file"); + return false; + } + + // Adds content types file ("[Content_Types].xml";). + // The content of this file is the same for each PrusaSlicer 3mf. + if (!_add_content_types_file_to_archive(archive)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + if (thumbnail_data != nullptr && thumbnail_data->is_valid()) { + // Adds the file Metadata/thumbnail.png. + if (!_add_thumbnail_file_to_archive(archive, *thumbnail_data)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + } + + // Adds relationships file ("_rels/.rels"). + // The content of this file is the same for each PrusaSlicer 3mf. + // The relationshis file contains a reference to the geometry file "3D/3dmodel.model", the name was chosen to be compatible with CURA. + if (!_add_relationships_file_to_archive(archive)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds model file ("3D/3dmodel.model"). + // This is the one and only file that contains all the geometry (vertices and triangles) of all ModelVolumes. + IdToObjectDataMap objects_data; + if (!_add_model_file_to_archive(filename, archive, model, objects_data)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds file with information for object cut ("Metadata/Slic3r_PE_cut_information.txt"). + // All information for object cut of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. + // The index differes from the index of an object ID of an object instance of a 3MF file! + if (!_add_cut_information_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds layer height profile file ("Metadata/Slic3r_PE_layer_heights_profile.txt"). + // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. + // The index differes from the index of an object ID of an object instance of a 3MF file! + if (!_add_layer_height_profile_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds layer config ranges file ("Metadata/Slic3r_PE_layer_config_ranges.txt"). + // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. + // The index differes from the index of an object ID of an object instance of a 3MF file! + if (!_add_layer_config_ranges_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds sla support points file ("Metadata/Slic3r_PE_sla_support_points.txt"). + // All sla support points of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. + // The index differes from the index of an object ID of an object instance of a 3MF file! + if (!_add_sla_support_points_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + if (!_add_sla_drain_holes_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + + // Adds custom gcode per height file ("Metadata/Prusa_Slicer_custom_gcode_per_print_z.xml"). + // All custom gcode per height of whole Model are stored here + if (!_add_custom_gcode_per_print_z_file_to_archive(archive, model, config)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds slic3r print config file ("Metadata/Slic3r_PE.config"). + // This file contains the content of FullPrintConfing / SLAFullPrintConfig. + if (config != nullptr) { + if (!_add_print_config_file_to_archive(archive, *config)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + } + + // Adds slic3r model config file ("Metadata/Slic3r_PE_model.config"). + // This file contains all the attributes of all ModelObjects and their ModelVolumes (names, parameter overrides). + // As there is just a single Indexed Triangle Set data stored per ModelObject, offsets of volumes into their respective Indexed Triangle Set data + // is stored here as well. + if (!_add_model_config_file_to_archive(archive, model, objects_data)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + if (!mz_zip_writer_finalize_archive(&archive)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + add_error("Unable to finalize the archive"); + return false; + } + + close_zip_writer(&archive); + + return true; + } + + bool _3MF_Exporter::_add_content_types_file_to_archive(mz_zip_archive& archive) + { + std::stringstream stream; + stream << "\n"; + stream << "\n"; + stream << " \n"; + stream << " \n"; + stream << " \n"; + stream << ""; + + std::string out = stream.str(); + + if (!mz_zip_writer_add_mem(&archive, CONTENT_TYPES_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add content types file to archive"); + return false; + } + + return true; + } + + bool _3MF_Exporter::_add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data) + { + bool res = false; + + size_t png_size = 0; + void* png_data = tdefl_write_image_to_png_file_in_memory_ex((const void*)thumbnail_data.pixels.data(), thumbnail_data.width, thumbnail_data.height, 4, &png_size, MZ_DEFAULT_LEVEL, 1); + if (png_data != nullptr) { + res = mz_zip_writer_add_mem(&archive, THUMBNAIL_FILE.c_str(), (const void*)png_data, png_size, MZ_DEFAULT_COMPRESSION); + mz_free(png_data); + } + + if (!res) + add_error("Unable to add thumbnail file to archive"); + + return res; + } + + bool _3MF_Exporter::_add_relationships_file_to_archive(mz_zip_archive& archive) + { + std::stringstream stream; + stream << "\n"; + stream << "\n"; + stream << " \n"; + stream << " \n"; + stream << ""; + + std::string out = stream.str(); + + if (!mz_zip_writer_add_mem(&archive, RELATIONSHIPS_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add relationships file to archive"); + return false; + } + + return true; + } + + static void reset_stream(std::stringstream &stream) + { + stream.str(""); + stream.clear(); + // https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10 + // Conversion of a floating-point value to text and back is exact as long as at least max_digits10 were used (9 for float, 17 for double). + // It is guaranteed to produce the same floating-point value, even though the intermediate text representation is not exact. + // The default value of std::stream precision is 6 digits only! + stream << std::setprecision(std::numeric_limits::max_digits10); + } + + bool _3MF_Exporter::_add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data) + { + mz_zip_writer_staged_context context; + if (!mz_zip_writer_add_staged_open(&archive, &context, MODEL_FILE.c_str(), + m_zip64 ? + // Maximum expected and allowed 3MF file size is 16GiB. + // This switches the ZIP file to a 64bit mode, which adds a tiny bit of overhead to file records. + (uint64_t(1) << 30) * 16 : + // Maximum expected 3MF file size is 4GB-1. This is a workaround for interoperability with Windows 10 3D model fixing API, see + // GH issue #6193. + (uint64_t(1) << 32) - 1, + nullptr, nullptr, 0, MZ_DEFAULT_COMPRESSION, nullptr, 0, nullptr, 0)) { + add_error("Unable to add model file to archive"); + return false; + } + + { + std::stringstream stream; + reset_stream(stream); + stream << "\n"; + stream << "<" << MODEL_TAG << " unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\" xmlns:slic3rpe=\"http://schemas.slic3r.org/3mf/2017/06\">\n"; + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_3MF_VERSION << "\">" << VERSION_3MF << "\n"; + + if (model.is_fdm_support_painted()) + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION << "\">" << FDM_SUPPORTS_PAINTING_VERSION << "\n"; + + if (model.is_seam_painted()) + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_SEAM_PAINTING_VERSION << "\">" << SEAM_PAINTING_VERSION << "\n"; + + if (model.is_mm_painted()) + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_MM_PAINTING_VERSION << "\">" << MM_PAINTING_VERSION << "\n"; + + std::string name = xml_escape(boost::filesystem::path(filename).stem().string()); + stream << " <" << METADATA_TAG << " name=\"Title\">" << name << "\n"; + stream << " <" << METADATA_TAG << " name=\"Designer\">" << "\n"; + stream << " <" << METADATA_TAG << " name=\"Description\">" << name << "\n"; + stream << " <" << METADATA_TAG << " name=\"Copyright\">" << "\n"; + stream << " <" << METADATA_TAG << " name=\"LicenseTerms\">" << "\n"; + stream << " <" << METADATA_TAG << " name=\"Rating\">" << "\n"; + std::string date = Slic3r::Utils::utc_timestamp(Slic3r::Utils::get_current_time_utc()); + // keep only the date part of the string + date = date.substr(0, 10); + stream << " <" << METADATA_TAG << " name=\"CreationDate\">" << date << "\n"; + stream << " <" << METADATA_TAG << " name=\"ModificationDate\">" << date << "\n"; + stream << " <" << METADATA_TAG << " name=\"Application\">" << SLIC3R_APP_KEY << "-" << SLIC3R_VERSION << "\n"; + stream << " <" << RESOURCES_TAG << ">\n"; + std::string buf = stream.str(); + if (! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) { + add_error("Unable to add model file to archive"); + return false; + } + } + + // Instance transformations, indexed by the 3MF object ID (which is a linear serialization of all instances of all ModelObjects). + BuildItemsList build_items; + + // The object_id here is a one based identifier of the first instance of a ModelObject in the 3MF file, where + // all the object instances of all ModelObjects are stored and indexed in a 1 based linear fashion. + // Therefore the list of object_ids here may not be continuous. + unsigned int object_id = 1; + for (ModelObject* obj : model.objects) { + if (obj == nullptr) + continue; + + // Index of an object in the 3MF file corresponding to the 1st instance of a ModelObject. + unsigned int curr_id = object_id; + IdToObjectDataMap::iterator object_it = objects_data.insert({ curr_id, ObjectData(obj) }).first; + // Store geometry of all ModelVolumes contained in a single ModelObject into a single 3MF indexed triangle set object. + // object_it->second.volumes_offsets will contain the offsets of the ModelVolumes in that single indexed triangle set. + // object_id will be increased to point to the 1st instance of the next ModelObject. + if (!_add_object_to_model_stream(context, object_id, *obj, build_items, object_it->second.volumes_offsets)) { + add_error("Unable to add object to archive"); + mz_zip_writer_add_staged_finish(&context); + return false; + } + } + + { + std::stringstream stream; + reset_stream(stream); + stream << " \n"; + + // Store the transformations of all the ModelInstances of all ModelObjects, indexed in a linear fashion. + if (!_add_build_to_model_stream(stream, build_items)) { + add_error("Unable to add build to archive"); + mz_zip_writer_add_staged_finish(&context); + return false; + } + + stream << "\n"; + + std::string buf = stream.str(); + + if ((! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) || + ! mz_zip_writer_add_staged_finish(&context)) { + add_error("Unable to add model file to archive"); + return false; + } + } + + return true; + } + + bool _3MF_Exporter::_add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets) + { + std::stringstream stream; + reset_stream(stream); + unsigned int id = 0; + for (const ModelInstance* instance : object.instances) { + assert(instance != nullptr); + if (instance == nullptr) + continue; + + unsigned int instance_id = object_id + id; + stream << " <" << OBJECT_TAG << " id=\"" << instance_id << "\" type=\"model\">\n"; + + if (id == 0) { + std::string buf = stream.str(); + reset_stream(stream); + if ((! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) || + ! _add_mesh_to_object_stream(context, object, volumes_offsets)) { + add_error("Unable to add mesh to archive"); + return false; + } + } + else { + stream << " <" << COMPONENTS_TAG << ">\n"; + stream << " <" << COMPONENT_TAG << " objectid=\"" << object_id << "\"/>\n"; + stream << " \n"; + } + + Transform3d t = instance->get_matrix(); + // instance_id is just a 1 indexed index in build_items. + assert(instance_id == build_items.size() + 1); + build_items.emplace_back(instance_id, t, instance->printable); + + stream << " \n"; + + ++id; + } + + object_id += id; + std::string buf = stream.str(); + return buf.empty() || mz_zip_writer_add_staged_data(&context, buf.data(), buf.size()); + } + +#if EXPORT_3MF_USE_SPIRIT_KARMA_FP + template + struct coordinate_policy_fixed : boost::spirit::karma::real_policies + { + static int floatfield(Num n) { return fmtflags::fixed; } + // Number of decimal digits to maintain float accuracy when storing into a text file and parsing back. + static unsigned precision(Num /* n */) { return std::numeric_limits::max_digits10 + 1; } + // No trailing zeros, thus for fmtflags::fixed usually much less than max_digits10 decimal numbers will be produced. + static bool trailing_zeros(Num /* n */) { return false; } + }; + template + struct coordinate_policy_scientific : coordinate_policy_fixed + { + static int floatfield(Num n) { return fmtflags::scientific; } + }; + // Define a new generator type based on the new coordinate policy. + using coordinate_type_fixed = boost::spirit::karma::real_generator>; + using coordinate_type_scientific = boost::spirit::karma::real_generator>; +#endif // EXPORT_3MF_USE_SPIRIT_KARMA_FP + + bool _3MF_Exporter::_add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets) + { + std::string output_buffer; + output_buffer += " <"; + output_buffer += MESH_TAG; + output_buffer += ">\n <"; + output_buffer += VERTICES_TAG; + output_buffer += ">\n"; + + auto flush = [this, &output_buffer, &context](bool force = false) { + if ((force && ! output_buffer.empty()) || output_buffer.size() >= 65536 * 16) { + if (! mz_zip_writer_add_staged_data(&context, output_buffer.data(), output_buffer.size())) { + add_error("Error during writing or compression"); + return false; + } + output_buffer.clear(); + } + return true; + }; + + auto format_coordinate = [](float f, char *buf) -> char* { + assert(is_decimal_separator_point()); +#if EXPORT_3MF_USE_SPIRIT_KARMA_FP + // Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, + // https://github.com/boostorg/spirit/pull/586 + // where the exported string is one digit shorter than it should be to guarantee lossless round trip. + // The code is left here for the ocasion boost guys improve. + coordinate_type_fixed const coordinate_fixed = coordinate_type_fixed(); + coordinate_type_scientific const coordinate_scientific = coordinate_type_scientific(); + // Format "f" in a fixed format. + char *ptr = buf; + boost::spirit::karma::generate(ptr, coordinate_fixed, f); + // Format "f" in a scientific format. + char *ptr2 = ptr; + boost::spirit::karma::generate(ptr2, coordinate_scientific, f); + // Return end of the shorter string. + auto len2 = ptr2 - ptr; + if (ptr - buf > len2) { + // Move the shorter scientific form to the front. + memcpy(buf, ptr, len2); + ptr = buf + len2; + } + // Return pointer to the end. + return ptr; +#else + // Round-trippable float, shortest possible. + return buf + sprintf(buf, "%.9g", f); +#endif + }; + + char buf[256]; + unsigned int vertices_count = 0; + for (ModelVolume* volume : object.volumes) { + if (volume == nullptr) + continue; + + volumes_offsets.insert({ volume, Offsets(vertices_count) }); + + const indexed_triangle_set &its = volume->mesh().its; + if (its.vertices.empty()) { + add_error("Found invalid mesh"); + return false; + } + + vertices_count += (int)its.vertices.size(); + + const Transform3d& matrix = volume->get_matrix(); + for (const auto& vertex: its.vertices) { + Vec3f v = (matrix * vertex.cast()).cast(); + char *ptr = buf; + boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << VERTEX_TAG << " x=\""); + ptr = format_coordinate(v.x(), ptr); + boost::spirit::karma::generate(ptr, "\" y=\""); + ptr = format_coordinate(v.y(), ptr); + boost::spirit::karma::generate(ptr, "\" z=\""); + ptr = format_coordinate(v.z(), ptr); + boost::spirit::karma::generate(ptr, "\"/>\n"); + *ptr = '\0'; + output_buffer += buf; + if (! flush()) + return false; + } + } + + output_buffer += " \n <"; + output_buffer += TRIANGLES_TAG; + output_buffer += ">\n"; + + unsigned int triangles_count = 0; + for (ModelVolume* volume : object.volumes) { + if (volume == nullptr) + continue; + + bool is_left_handed = volume->is_left_handed(); + VolumeToOffsetsMap::iterator volume_it = volumes_offsets.find(volume); + assert(volume_it != volumes_offsets.end()); + + const indexed_triangle_set &its = volume->mesh().its; + + // updates triangle offsets + volume_it->second.first_triangle_id = triangles_count; + triangles_count += (int)its.indices.size(); + volume_it->second.last_triangle_id = triangles_count - 1; + + for (int i = 0; i < int(its.indices.size()); ++ i) { + { + const Vec3i &idx = its.indices[i]; + char *ptr = buf; + boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << TRIANGLE_TAG << + " v1=\"" << boost::spirit::int_ << + "\" v2=\"" << boost::spirit::int_ << + "\" v3=\"" << boost::spirit::int_ << "\"", + idx[is_left_handed ? 2 : 0] + volume_it->second.first_vertex_id, + idx[1] + volume_it->second.first_vertex_id, + idx[is_left_handed ? 0 : 2] + volume_it->second.first_vertex_id); + *ptr = '\0'; + output_buffer += buf; + } + + std::string custom_supports_data_string = volume->supported_facets.get_triangle_as_string(i); + if (! custom_supports_data_string.empty()) { + output_buffer += " "; + output_buffer += CUSTOM_SUPPORTS_ATTR; + output_buffer += "=\""; + output_buffer += custom_supports_data_string; + output_buffer += "\""; + } + + std::string custom_seam_data_string = volume->seam_facets.get_triangle_as_string(i); + if (! custom_seam_data_string.empty()) { + output_buffer += " "; + output_buffer += CUSTOM_SEAM_ATTR; + output_buffer += "=\""; + output_buffer += custom_seam_data_string; + output_buffer += "\""; + } + + std::string mmu_painting_data_string = volume->mmu_segmentation_facets.get_triangle_as_string(i); + if (! mmu_painting_data_string.empty()) { + output_buffer += " "; + output_buffer += MMU_SEGMENTATION_ATTR; + output_buffer += "=\""; + output_buffer += mmu_painting_data_string; + output_buffer += "\""; + } + + output_buffer += "/>\n"; + + if (! flush()) + return false; + } + } + + output_buffer += " \n \n"; + + // Force flush. + return flush(true); + } + + void _3MF_Exporter::add_transformation(std::stringstream &stream, const Transform3d &tr) + { + for (unsigned c = 0; c < 4; ++c) { + for (unsigned r = 0; r < 3; ++r) { + stream << tr(r, c); + if (r != 2 || c != 3) stream << " "; + } + } + } + + bool _3MF_Exporter::_add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) + { + // This happens for empty projects + if (build_items.size() == 0) { + add_error("No build item found"); + return true; + } + + stream << " <" << BUILD_TAG << ">\n"; + + for (const BuildItem& item : build_items) { + stream << " <" << ITEM_TAG << " " << OBJECTID_ATTR << "=\"" << item.id << "\" " << TRANSFORM_ATTR << "=\""; + add_transformation(stream, item.transform); + stream << "\" " << PRINTABLE_ATTR << "=\"" << item.printable << "\"/>\n"; + } + + stream << " \n"; + + return true; + } + + bool _3MF_Exporter::_add_cut_information_file_to_archive(mz_zip_archive& archive, Model& model) + { + std::string out = ""; + pt::ptree tree; + + unsigned int object_cnt = 0; + for (const ModelObject* object : model.objects) { + object_cnt++; + pt::ptree& obj_tree = tree.add("objects.object", ""); + + obj_tree.put(".id", object_cnt); + + // Store info for cut_id + pt::ptree& cut_id_tree = obj_tree.add("cut_id", ""); + + // store cut_id atributes + cut_id_tree.put(".id", object->cut_id.id().id); + cut_id_tree.put(".check_sum", object->cut_id.check_sum()); + cut_id_tree.put(".connectors_cnt", object->cut_id.connectors_cnt()); + + int volume_idx = -1; + for (const ModelVolume* volume : object->volumes) { + ++volume_idx; + if (volume->is_cut_connector()) { + pt::ptree& connectors_tree = obj_tree.add("connectors.connector", ""); + connectors_tree.put(".volume_id", volume_idx); + connectors_tree.put(".type", int(volume->cut_info.connector_type)); + connectors_tree.put(".r_tolerance", volume->cut_info.radius_tolerance); + connectors_tree.put(".h_tolerance", volume->cut_info.height_tolerance); + } + } + } + + if (!tree.empty()) { + std::ostringstream oss; + pt::write_xml(oss, tree); + out = oss.str(); + + // Post processing("beautification") of the output string for a better preview + boost::replace_all(out, ">\n \n ", ">\n "); + boost::replace_all(out, ">\n ", ">\n "); + boost::replace_all(out, ">\n ", ">\n "); + boost::replace_all(out, ">", ">\n "); + // OR just + boost::replace_all(out, "><", ">\n<"); + } + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, CUT_INFORMATION_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add cut information file to archive"); + return false; + } + } + + return true; + } + + bool _3MF_Exporter::_add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model) + { + assert(is_decimal_separator_point()); + std::string out = ""; + char buffer[1024]; + + unsigned int count = 0; + for (const ModelObject* object : model.objects) { + ++count; + const std::vector& layer_height_profile = object->layer_height_profile.get(); + if (layer_height_profile.size() >= 4 && layer_height_profile.size() % 2 == 0) { + sprintf(buffer, "object_id=%d|", count); + out += buffer; + + // Store the layer height profile as a single semicolon separated list. + for (size_t i = 0; i < layer_height_profile.size(); ++i) { + sprintf(buffer, (i == 0) ? "%f" : ";%f", layer_height_profile[i]); + out += buffer; + } + + out += "\n"; + } + } + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, LAYER_HEIGHTS_PROFILE_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add layer heights profile file to archive"); + return false; + } + } + + return true; + } + + bool _3MF_Exporter::_add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model) + { + std::string out = ""; + pt::ptree tree; + + unsigned int object_cnt = 0; + for (const ModelObject* object : model.objects) { + object_cnt++; + const t_layer_config_ranges& ranges = object->layer_config_ranges; + if (!ranges.empty()) + { + pt::ptree& obj_tree = tree.add("objects.object",""); + + obj_tree.put(".id", object_cnt); + + // Store the layer config ranges. + for (const auto& range : ranges) { + pt::ptree& range_tree = obj_tree.add("range", ""); + + // store minX and maxZ + range_tree.put(".min_z", range.first.first); + range_tree.put(".max_z", range.first.second); + + // store range configuration + const ModelConfig& config = range.second; + for (const std::string& opt_key : config.keys()) { + pt::ptree& opt_tree = range_tree.add("option", config.opt_serialize(opt_key)); + opt_tree.put(".opt_key", opt_key); + } + } + } + } + + if (!tree.empty()) { + std::ostringstream oss; + pt::write_xml(oss, tree); + out = oss.str(); + + // Post processing("beautification") of the output string for a better preview + boost::replace_all(out, ">\n \n \n ", ">\n "); + boost::replace_all(out, ">", ">\n "); + // OR just + boost::replace_all(out, "><", ">\n<"); + } + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, LAYER_CONFIG_RANGES_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add layer heights profile file to archive"); + return false; + } + } + + return true; + } + + bool _3MF_Exporter::_add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model) + { + assert(is_decimal_separator_point()); + std::string out = ""; + char buffer[1024]; + + unsigned int count = 0; + for (const ModelObject* object : model.objects) { + ++count; + const std::vector& sla_support_points = object->sla_support_points; + if (!sla_support_points.empty()) { + sprintf(buffer, "object_id=%d|", count); + out += buffer; + + // Store the layer height profile as a single space separated list. + for (size_t i = 0; i < sla_support_points.size(); ++i) { + sprintf(buffer, (i==0 ? "%f %f %f %f %f" : " %f %f %f %f %f"), sla_support_points[i].pos(0), sla_support_points[i].pos(1), sla_support_points[i].pos(2), sla_support_points[i].head_front_radius, (float)sla_support_points[i].is_new_island); + out += buffer; + } + out += "\n"; + } + } + + if (!out.empty()) { + // Adds version header at the beginning: + out = std::string("support_points_format_version=") + std::to_string(support_points_format_version) + std::string("\n") + out; + + if (!mz_zip_writer_add_mem(&archive, SLA_SUPPORT_POINTS_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add sla support points file to archive"); + return false; + } + } + return true; + } + + bool _3MF_Exporter::_add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model) + { + assert(is_decimal_separator_point()); + const char *const fmt = "object_id=%d|"; + std::string out; + + unsigned int count = 0; + for (const ModelObject* object : model.objects) { + ++count; + sla::DrainHoles drain_holes = object->sla_drain_holes; + + // The holes were placed 1mm above the mesh in the first implementation. + // This was a bad idea and the reference point was changed in 2.3 so + // to be on the mesh exactly. The elevated position is still saved + // in 3MFs for compatibility reasons. + for (sla::DrainHole& hole : drain_holes) { + hole.pos -= hole.normal.normalized(); + hole.height += 1.f; + } + + if (!drain_holes.empty()) { + out += string_printf(fmt, count); + + // Store the layer height profile as a single space separated list. + for (size_t i = 0; i < drain_holes.size(); ++i) + out += string_printf((i == 0 ? "%f %f %f %f %f %f %f %f" : " %f %f %f %f %f %f %f %f"), + drain_holes[i].pos(0), + drain_holes[i].pos(1), + drain_holes[i].pos(2), + drain_holes[i].normal(0), + drain_holes[i].normal(1), + drain_holes[i].normal(2), + drain_holes[i].radius, + drain_holes[i].height); + + out += "\n"; + } + } + + if (!out.empty()) { + // Adds version header at the beginning: + out = std::string("drain_holes_format_version=") + std::to_string(drain_holes_format_version) + std::string("\n") + out; + + if (!mz_zip_writer_add_mem(&archive, SLA_DRAIN_HOLES_FILE.c_str(), static_cast(out.data()), out.length(), mz_uint(MZ_DEFAULT_COMPRESSION))) { + add_error("Unable to add sla support points file to archive"); + return false; + } + } + return true; + } + + bool _3MF_Exporter::_add_print_config_file_to_archive(mz_zip_archive& archive, const DynamicPrintConfig &config) + { + assert(is_decimal_separator_point()); + char buffer[1024]; + sprintf(buffer, "; %s\n\n", header_slic3r_generated().c_str()); + std::string out = buffer; + + for (const std::string &key : config.keys()) + if (key != "compatible_printers") + out += "; " + key + " = " + config.opt_serialize(key) + "\n"; + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, PRINT_CONFIG_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add print config file to archive"); + return false; + } + } + + return true; + } + + bool _3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, const IdToObjectDataMap &objects_data) + { + enum class MetadataType{ + object, + volume + }; + + auto add_metadata = [](std::stringstream &stream, unsigned indent, MetadataType type, + const std::string &key, const std::string &value) { + const char *type_value; + switch (type) { + case MetadataType::object: type_value = OBJECT_TYPE; break; + case MetadataType::volume: type_value = VOLUME_TYPE; break; + }; + stream << std::string(indent, ' ') << '<' << METADATA_TAG << " " + << TYPE_ATTR << "=\"" << type_value << "\" " + << KEY_ATTR << "=\"" << key << "\" " + << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(value) << "\"/>\n"; + }; + + std::stringstream stream; + // Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back + // when loaded as accurately as possible. + stream << std::setprecision(std::numeric_limits::max_digits10); + stream << "\n"; + stream << "<" << CONFIG_TAG << ">\n"; + + for (const IdToObjectDataMap::value_type& obj_metadata : objects_data) { + const ModelObject* obj = obj_metadata.second.object; + if (obj == nullptr) continue; + // Output of instances count added because of github #3435, currently not used by PrusaSlicer + stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << obj_metadata.first << "\" " << INSTANCESCOUNT_ATTR << "=\"" << obj->instances.size() << "\">\n"; + + // stores object's name + if (!obj->name.empty()) + add_metadata(stream, 2, MetadataType::object, "name", obj->name); + // stores object's config data + const ModelConfigObject &config = obj->config; + for (const std::string& key : config.keys()) + add_metadata(stream, 2, MetadataType::object, key, config.opt_serialize(key)); + + for (const ModelVolume* volume : obj_metadata.second.object->volumes) { + if (volume == nullptr) continue; + const VolumeToOffsetsMap& offsets = obj_metadata.second.volumes_offsets; + VolumeToOffsetsMap::const_iterator it = offsets.find(volume); + if (it != offsets.end()) { + // stores volume's offsets + stream << " <" << VOLUME_TAG << " "; + stream << FIRST_TRIANGLE_ID_ATTR << "=\"" << it->second.first_triangle_id << "\" "; + stream << LAST_TRIANGLE_ID_ATTR << "=\"" << it->second.last_triangle_id << "\">\n"; + + // stores volume's name + if (!volume->name.empty()) + add_metadata(stream, 3, MetadataType::volume, NAME_KEY, volume->name); + + // stores volume's modifier field (legacy, to support old slicers) + if (volume->is_modifier()) + add_metadata(stream, 3, MetadataType::volume, MODIFIER_KEY, "1"); + // stores volume's type (overrides the modifier field above) + add_metadata(stream, 3, MetadataType::volume, VOLUME_TYPE_KEY, ModelVolume::type_to_string(volume->type())); + + // stores volume's local matrix + stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MATRIX_KEY << "\" " << VALUE_ATTR << "=\""; + const Transform3d matrix = volume->get_matrix() * volume->source.transform.get_matrix(); + for (int r = 0; r < 4; ++r) { + for (int c = 0; c < 4; ++c) { + stream << matrix(r, c); + if (r != 3 || c != 3) + stream << " "; + } + } + stream << "\"/>\n"; + + // stores volume's source data + { + std::string input_file = xml_escape(m_fullpath_sources ? volume->source.input_file : boost::filesystem::path(volume->source.input_file).filename().string()); + std::string prefix = std::string(" <") + METADATA_TAG + " " + TYPE_ATTR + "=\"" + VOLUME_TYPE + "\" " + KEY_ATTR + "=\""; + if (! volume->source.input_file.empty()) { + stream << prefix << SOURCE_FILE_KEY << "\" " << VALUE_ATTR << "=\"" << input_file << "\"/>\n"; + stream << prefix << SOURCE_OBJECT_ID_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.object_idx << "\"/>\n"; + stream << prefix << SOURCE_VOLUME_ID_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.volume_idx << "\"/>\n"; + stream << prefix << SOURCE_OFFSET_X_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(0) << "\"/>\n"; + stream << prefix << SOURCE_OFFSET_Y_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(1) << "\"/>\n"; + stream << prefix << SOURCE_OFFSET_Z_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(2) << "\"/>\n"; + } + assert(! volume->source.is_converted_from_inches || ! volume->source.is_converted_from_meters); + if (volume->source.is_converted_from_inches) + stream << prefix << SOURCE_IN_INCHES_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; + else if (volume->source.is_converted_from_meters) + stream << prefix << SOURCE_IN_METERS_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; +#if ENABLE_RELOAD_FROM_DISK_REWORK + if (volume->source.is_from_builtin_objects) + stream << prefix << SOURCE_IS_BUILTIN_VOLUME_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + } + + // stores volume's config data + for (const std::string& key : volume->config.keys()) { + stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; + } + + // stores volume's text data + const auto &tc = volume->text_configuration; + if (tc.has_value()) + TextConfigurationSerialization::create_fix_and_store(stream, *tc, *volume); + + // stores mesh's statistics + const RepairedMeshErrors& stats = volume->mesh().stats().repaired_errors; + stream << " <" << MESH_TAG << " "; + stream << MESH_STAT_EDGES_FIXED << "=\"" << stats.edges_fixed << "\" "; + stream << MESH_STAT_DEGENERATED_FACETS << "=\"" << stats.degenerate_facets << "\" "; + stream << MESH_STAT_FACETS_REMOVED << "=\"" << stats.facets_removed << "\" "; + stream << MESH_STAT_FACETS_RESERVED << "=\"" << stats.facets_reversed << "\" "; + stream << MESH_STAT_BACKWARDS_EDGES << "=\"" << stats.backwards_edges << "\"/>\n"; + + stream << " \n"; + } + } + stream << " \n"; + } + + stream << "\n"; + + std::string out = stream.str(); + + if (!mz_zip_writer_add_mem(&archive, MODEL_CONFIG_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add model config file to archive"); + return false; + } + + return true; + } + +bool _3MF_Exporter::_add_custom_gcode_per_print_z_file_to_archive( mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config) +{ + std::string out = ""; + + if (!model.custom_gcode_per_print_z.gcodes.empty()) { + pt::ptree tree; + pt::ptree& main_tree = tree.add("custom_gcodes_per_print_z", ""); + + for (const CustomGCode::Item& code : model.custom_gcode_per_print_z.gcodes) { + pt::ptree& code_tree = main_tree.add("code", ""); + + // store data of custom_gcode_per_print_z + code_tree.put(".print_z" , code.print_z ); + code_tree.put(".type" , static_cast(code.type)); + code_tree.put(".extruder" , code.extruder ); + code_tree.put(".color" , code.color ); + code_tree.put(".extra" , code.extra ); + + // add gcode field data for the old version of the PrusaSlicer + std::string gcode = code.type == CustomGCode::ColorChange ? config->opt_string("color_change_gcode") : + code.type == CustomGCode::PausePrint ? config->opt_string("pause_print_gcode") : + code.type == CustomGCode::Template ? config->opt_string("template_custom_gcode") : + code.type == CustomGCode::ToolChange ? "tool_change" : code.extra; + code_tree.put(".gcode" , gcode ); + } + + pt::ptree& mode_tree = main_tree.add("mode", ""); + // store mode of a custom_gcode_per_print_z + mode_tree.put(".value", model.custom_gcode_per_print_z.mode == CustomGCode::Mode::SingleExtruder ? CustomGCode::SingleExtruderMode : + model.custom_gcode_per_print_z.mode == CustomGCode::Mode::MultiAsSingle ? CustomGCode::MultiAsSingleMode : + CustomGCode::MultiExtruderMode); + + if (!tree.empty()) { + std::ostringstream oss; + boost::property_tree::write_xml(oss, tree); + out = oss.str(); + + // Post processing("beautification") of the output string + boost::replace_all(out, "><", ">\n<"); + } + } + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, CUSTOM_GCODE_PER_PRINT_Z_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add custom Gcodes per print_z file to archive"); + return false; + } + } + + return true; +} + +// Perform conversions based on the config values available. +static void handle_legacy_project_loaded(unsigned int version_project_file, DynamicPrintConfig& config, const boost::optional& prusaslicer_generator_version) +{ + if (! config.has("brim_separation")) { + if (auto *opt_elephant_foot = config.option("elefant_foot_compensation", false); opt_elephant_foot) { + // Conversion from older PrusaSlicer which applied brim separation equal to elephant foot compensation. + auto *opt_brim_separation = config.option("brim_separation", true); + opt_brim_separation->value = opt_elephant_foot->value; + } + } + + // In PrusaSlicer 2.5.0-alpha2 and 2.5.0-alpha3, we introduce several parameters for Arachne that depend + // on nozzle size . Later we decided to make default values for those parameters computed automatically + // until the user changes them. + if (prusaslicer_generator_version && *prusaslicer_generator_version >= *Semver::parse("2.5.0-alpha2") && *prusaslicer_generator_version <= *Semver::parse("2.5.0-alpha3")) { + if (auto *opt_wall_transition_length = config.option("wall_transition_length", false); + opt_wall_transition_length && !opt_wall_transition_length->percent && opt_wall_transition_length->value == 0.4) { + opt_wall_transition_length->percent = true; + opt_wall_transition_length->value = 100; + } + + if (auto *opt_min_feature_size = config.option("min_feature_size", false); + opt_min_feature_size && !opt_min_feature_size->percent && opt_min_feature_size->value == 0.1) { + opt_min_feature_size->percent = true; + opt_min_feature_size->value = 25; + } + } +} + +bool is_project_3mf(const std::string& filename) +{ + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_reader(&archive, filename)) + return false; + + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + + // loop the entries to search for config + mz_zip_archive_file_stat stat; + bool config_found = false; + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + std::string name(stat.m_filename); + std::replace(name.begin(), name.end(), '\\', '/'); + + if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { + config_found = true; + break; + } + } + } + + close_zip_reader(&archive); + + return config_found; +} + +bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Model* model, bool check_version) +{ + if (path == nullptr || model == nullptr) + return false; + + // All import should use "C" locales for number formatting. + CNumericLocalesSetter locales_setter; + _3MF_Importer importer; + importer.load_model_from_file(path, *model, config, config_substitutions, check_version); + importer.log_errors(); + handle_legacy_project_loaded(importer.version(), config, importer.prusaslicer_generator_version()); + + return !model->objects.empty() || !config.empty(); +} + +bool store_3mf(const char* path, Model* model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64) +{ + // All export should use "C" locales for number formatting. + CNumericLocalesSetter locales_setter; + + if (path == nullptr || model == nullptr) + return false; + + _3MF_Exporter exporter; + bool res = exporter.save_model_to_file(path, *model, config, fullpath_sources, thumbnail_data, zip64); + if (!res) + exporter.log_errors(); + + return res; +} + +/// +/// TextConfiguration serialization +/// +using TypeToName = boost::bimap; +const TypeToName TextConfigurationSerialization::type_to_name = + boost::assign::list_of + (EmbossStyle::Type::file_path, "file_name") + (EmbossStyle::Type::wx_win_font_descr, "wxFontDescriptor_Windows") + (EmbossStyle::Type::wx_lin_font_descr, "wxFontDescriptor_Linux") + (EmbossStyle::Type::wx_mac_font_descr, "wxFontDescriptor_MacOsX"); + +void TextConfigurationSerialization::to_xml(std::stringstream &stream, const TextConfiguration &tc) +{ + stream << " <" << TEXT_TAG << " "; + + stream << TEXT_DATA_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(tc.text) << "\" "; + // font item + const EmbossStyle &fi = tc.style; + stream << STYLE_NAME_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(fi.name) << "\" "; + stream << FONT_DESCRIPTOR_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(fi.path) << "\" "; + stream << FONT_DESCRIPTOR_TYPE_ATTR << "=\"" << TextConfigurationSerialization::get_name(fi.type) << "\" "; + + // font property + const FontProp &fp = tc.style.prop; + if (fp.char_gap.has_value()) + stream << CHAR_GAP_ATTR << "=\"" << *fp.char_gap << "\" "; + if (fp.line_gap.has_value()) + stream << LINE_GAP_ATTR << "=\"" << *fp.line_gap << "\" "; + + stream << LINE_HEIGHT_ATTR << "=\"" << fp.size_in_mm << "\" "; + stream << DEPTH_ATTR << "=\"" << fp.emboss << "\" "; + if (fp.use_surface) + stream << USE_SURFACE_ATTR << "=\"" << 1 << "\" "; + if (fp.boldness.has_value()) + stream << BOLDNESS_ATTR << "=\"" << *fp.boldness << "\" "; + if (fp.skew.has_value()) + stream << SKEW_ATTR << "=\"" << *fp.skew << "\" "; + if (fp.distance.has_value()) + stream << DISTANCE_ATTR << "=\"" << *fp.distance << "\" "; + if (fp.angle.has_value()) + stream << ANGLE_ATTR << "=\"" << *fp.angle << "\" "; + if (fp.collection_number.has_value()) + stream << COLLECTION_NUMBER_ATTR << "=\"" << *fp.collection_number << "\" "; + // font descriptor + if (fp.family.has_value()) + stream << FONT_FAMILY_ATTR << "=\"" << *fp.family << "\" "; + if (fp.face_name.has_value()) + stream << FONT_FACE_NAME_ATTR << "=\"" << *fp.face_name << "\" "; + if (fp.style.has_value()) + stream << FONT_STYLE_ATTR << "=\"" << *fp.style << "\" "; + if (fp.weight.has_value()) + stream << FONT_WEIGHT_ATTR << "=\"" << *fp.weight << "\" "; + + // FIX of baked transformation + assert(tc.fix_3mf_tr.has_value()); + stream << TRANSFORM_ATTR << "=\""; + _3MF_Exporter::add_transformation(stream, *tc.fix_3mf_tr); + stream << "\" "; + + stream << "/>\n"; // end TEXT_TAG +} + +void TextConfigurationSerialization::create_fix_and_store( + std::stringstream &stream, TextConfiguration tc, const ModelVolume &volume) +{ + const auto& vertices = volume.mesh().its.vertices; + assert(!vertices.empty()); + if (vertices.empty()) { + to_xml(stream, tc); + return; + } + + // IMPROVE: check if volume was modified (translated, rotated OR scaled) + // when no change do not calculate transformation only store original fix matrix + + // Create transformation used after load actual stored volume + const Transform3d &actual_trmat = volume.get_transformation().get_matrix(); + Vec3d min = actual_trmat * vertices.front().cast(); + Vec3d max = min; + for (const Vec3f &v : vertices) { + Vec3d vd = actual_trmat * v.cast(); + for (size_t i = 0; i < 3; ++i) { + if (min[i] > vd[i]) min[i] = vd[i]; + if (max[i] < vd[i]) max[i] = vd[i]; + } + } + Vec3d center = (max + min) / 2; + Transform3d post_trmat = Transform3d::Identity(); + post_trmat.translate(center); + + Transform3d fix_trmat = actual_trmat.inverse() * post_trmat; + if (!tc.fix_3mf_tr.has_value()) { + tc.fix_3mf_tr = fix_trmat; + } else if (!fix_trmat.isApprox(Transform3d::Identity(), 1e-5)) { + tc.fix_3mf_tr = *tc.fix_3mf_tr * fix_trmat; + } + to_xml(stream, tc); +} + +std::optional TextConfigurationSerialization::read(const char **attributes, unsigned int num_attributes) +{ + FontProp fp; + int char_gap = get_attribute_value_int(attributes, num_attributes, CHAR_GAP_ATTR); + if (char_gap != 0) fp.char_gap = char_gap; + int line_gap = get_attribute_value_int(attributes, num_attributes, LINE_GAP_ATTR); + if (line_gap != 0) fp.line_gap = line_gap; + float boldness = get_attribute_value_float(attributes, num_attributes, BOLDNESS_ATTR); + if (std::fabs(boldness) > std::numeric_limits::epsilon()) + fp.boldness = boldness; + float skew = get_attribute_value_float(attributes, num_attributes, SKEW_ATTR); + if (std::fabs(skew) > std::numeric_limits::epsilon()) + fp.skew = skew; + float distance = get_attribute_value_float(attributes, num_attributes, DISTANCE_ATTR); + if (std::fabs(distance) > std::numeric_limits::epsilon()) + fp.distance = distance; + std::string use_surface = get_attribute_value_string(attributes, num_attributes, USE_SURFACE_ATTR); + if (!use_surface.empty()) fp.use_surface = true; + float angle = get_attribute_value_float(attributes, num_attributes, ANGLE_ATTR); + if (std::fabs(angle) > std::numeric_limits::epsilon()) + fp.angle = angle; + int collection_number = get_attribute_value_int(attributes, num_attributes, COLLECTION_NUMBER_ATTR); + if (collection_number > 0) fp.collection_number = static_cast(collection_number); + + fp.size_in_mm = get_attribute_value_float(attributes, num_attributes, LINE_HEIGHT_ATTR); + fp.emboss = get_attribute_value_float(attributes, num_attributes, DEPTH_ATTR); + + std::string family = get_attribute_value_string(attributes, num_attributes, FONT_FAMILY_ATTR); + if (!family.empty()) fp.family = family; + std::string face_name = get_attribute_value_string(attributes, num_attributes, FONT_FACE_NAME_ATTR); + if (!face_name.empty()) fp.face_name = face_name; + std::string style = get_attribute_value_string(attributes, num_attributes, FONT_STYLE_ATTR); + if (!style.empty()) fp.style = style; + std::string weight = get_attribute_value_string(attributes, num_attributes, FONT_WEIGHT_ATTR); + if (!weight.empty()) fp.weight = weight; + + std::string style_name = get_attribute_value_string(attributes, num_attributes, STYLE_NAME_ATTR); + std::string font_descriptor = get_attribute_value_string(attributes, num_attributes, FONT_DESCRIPTOR_ATTR); + std::string type_str = get_attribute_value_string(attributes, num_attributes, FONT_DESCRIPTOR_TYPE_ATTR); + EmbossStyle::Type type = TextConfigurationSerialization::get_type(type_str); + EmbossStyle fi{ style_name, std::move(font_descriptor), type, std::move(fp) }; + + std::string text = get_attribute_value_string(attributes, num_attributes, TEXT_DATA_ATTR); + + std::optional fix_tr_mat; + std::string fix_tr_mat_str = get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR); + if (!fix_tr_mat_str.empty()) { + fix_tr_mat = get_transform_from_3mf_specs_string(fix_tr_mat_str); + } + + return TextConfiguration{std::move(fi), std::move(text), std::move(fix_tr_mat)}; +} + + +} // namespace Slic3r diff --git a/src/libslic3r/IntersectionPoints.cpp b/src/libslic3r/IntersectionPoints.cpp new file mode 100644 index 0000000000..f2c63a53bf --- /dev/null +++ b/src/libslic3r/IntersectionPoints.cpp @@ -0,0 +1,170 @@ +#include "IntersectionPoints.hpp" + +//#define USE_CGAL_SWEEP_LINE +#ifdef USE_CGAL_SWEEP_LINE + +#include +#include +#include +#include +#include + +using NT = CGAL::Quotient; +using Kernel = CGAL::Cartesian; +using P2 = Kernel::Point_2; +using Traits_2 = CGAL::Arr_segment_traits_2; +using Segment = Traits_2::Curve_2; +using Segments = std::vector; + +namespace priv { + +P2 convert(const Slic3r::Point &p) { return P2(p.x(), p.y()); } +Slic3r::Vec2d convert(const P2 &p) +{ + return Slic3r::Vec2d(CGAL::to_double(p.x()), CGAL::to_double(p.y())); +} + +Slic3r::Pointfs compute_intersections(const Segments &segments) +{ + std::vector intersections; + // Compute all intersection points. + CGAL::compute_intersection_points(segments.begin(), segments.end(), + std::back_inserter(intersections)); + if (intersections.empty()) return {}; + Slic3r::Pointfs pts; + pts.reserve(intersections.size()); + for (const P2 &p : intersections) pts.push_back(convert(p)); + return pts; +} + +void add_polygon(const Slic3r::Polygon &polygon, Segments &segments) +{ + if (polygon.points.size() < 2) return; + P2 prev_point = priv::convert(polygon.last_point()); + for (const Slic3r::Point &p : polygon.points) { + P2 act_point = priv::convert(p); + if (prev_point == act_point) continue; + segments.emplace_back(prev_point, act_point); + prev_point = act_point; + } +} +Slic3r::Pointfs Slic3r::intersection_points(const Lines &lines) +{ + return priv::compute_intersections2(lines); + Segments segments; + segments.reserve(lines.size()); + for (Line l : lines) + segments.emplace_back(priv::convert(l.a), priv::convert(l.b)); + return priv::compute_intersections(segments); +} + +Slic3r::Pointfs Slic3r::intersection_points(const Polygon &polygon) +{ + Segments segments; + segments.reserve(polygon.points.size()); + priv::add_polygon(polygon, segments); + return priv::compute_intersections(segments); +} + +Slic3r::Pointfs Slic3r::intersection_points(const Polygons &polygons) +{ + Segments segments; + segments.reserve(count_points(polygons)); + for (const Polygon &polygon : polygons) + priv::add_polygon(polygon, segments); + return priv::compute_intersections(segments); +} + +Slic3r::Pointfs Slic3r::intersection_points(const ExPolygon &expolygon) +{ + Segments segments; + segments.reserve(count_points(expolygon)); + priv::add_polygon(expolygon.contour, segments); + for (const Polygon &hole : expolygon.holes) + priv::add_polygon(hole, segments); + return priv::compute_intersections(segments); +} + +Slic3r::Pointfs Slic3r::intersection_points(const ExPolygons &expolygons) +{ + Segments segments; + segments.reserve(count_points(expolygons)); + for (const ExPolygon &expolygon : expolygons) { + priv::add_polygon(expolygon.contour, segments); + for (const Polygon &hole : expolygon.holes) + priv::add_polygon(hole, segments); + } + return priv::compute_intersections(segments); +} + + +} // namespace priv + +#else // USE_CGAL_SWEEP_LINE + +// use bounding boxes +#include + +namespace priv { +Slic3r::Pointfs compute_intersections(const Slic3r::Lines &lines) +{ + using namespace Slic3r; + // IMPROVE0: BoundingBoxes of Polygons + // IMPROVE1: Polygon's neighbor lines can't intersect + // e.g. use indices to Point to find same points + // IMPROVE2: Use BentleyOttmann algorithm + // https://doc.cgal.org/latest/Surface_sweep_2/index.html -- CGAL implementation is significantly slower + // https://stackoverflow.com/questions/4407493/is-there-a-robust-c-implementation-of-the-bentley-ottmann-algorithm + Pointfs pts; + Point i; + for (size_t li = 0; li < lines.size(); ++li) { + const Line &l = lines[li]; + const Point &a = l.a; + const Point &b = l.b; + Point min(std::min(a.x(), b.x()), std::min(a.y(), b.y())); + Point max(std::max(a.x(), b.x()), std::max(a.y(), b.y())); + BoundingBox bb(min, max); + for (size_t li_ = li + 1; li_ < lines.size(); ++li_) { + const Line &l_ = lines[li_]; + const Point &a_ = l_.a; + const Point &b_ = l_.b; + if (a == b_ || b == a_ || a == a_ || b == b_) continue; + Point min_(std::min(a_.x(), b_.x()), std::min(a_.y(), b_.y())); + Point max_(std::max(a_.x(), b_.x()), std::max(a_.y(), b_.y())); + BoundingBox bb_(min_, max_); + // intersect of BB compare min max + if (bb.intersects(bb_) && + l.intersection(l_, &i)) + pts.push_back(i.cast()); + } + } + return pts; +} +} // namespace priv + +Slic3r::Pointfs Slic3r::intersection_points(const Lines &lines) +{ + return priv::compute_intersections(lines); +} + +Slic3r::Pointfs Slic3r::intersection_points(const Polygon &polygon) +{ + return priv::compute_intersections(to_lines(polygon)); +} + +Slic3r::Pointfs Slic3r::intersection_points(const Polygons &polygons) +{ + return priv::compute_intersections(to_lines(polygons)); +} + +Slic3r::Pointfs Slic3r::intersection_points(const ExPolygon &expolygon) +{ + return priv::compute_intersections(to_lines(expolygon)); +} + +Slic3r::Pointfs Slic3r::intersection_points(const ExPolygons &expolygons) +{ + return priv::compute_intersections(to_lines(expolygons)); +} + +#endif // USE_CGAL_SWEEP_LINE diff --git a/src/libslic3r/IntersectionPoints.hpp b/src/libslic3r/IntersectionPoints.hpp new file mode 100644 index 0000000000..2b1d3aa732 --- /dev/null +++ b/src/libslic3r/IntersectionPoints.hpp @@ -0,0 +1,16 @@ +#ifndef slic3r_IntersectionPoints_hpp_ +#define slic3r_IntersectionPoints_hpp_ + +#include "ExPolygon.hpp" + +namespace Slic3r { + +// collect all intersecting points +Pointfs intersection_points(const Lines &lines); +Pointfs intersection_points(const Polygon &polygon); +Pointfs intersection_points(const Polygons &polygons); +Pointfs intersection_points(const ExPolygon &expolygon); +Pointfs intersection_points(const ExPolygons &expolygons); + +} // namespace Slic3r +#endif // slic3r_IntersectionPoints_hpp_ \ No newline at end of file diff --git a/src/libslic3r/Line.hpp b/src/libslic3r/Line.hpp index f7a14c2510..947c7fcbaf 100644 --- a/src/libslic3r/Line.hpp +++ b/src/libslic3r/Line.hpp @@ -210,6 +210,7 @@ public: static const constexpr int Dim = 2; using Scalar = Vec2d::Scalar; }; +using Linesf = std::vector; class Linef3 { diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index ee611b0066..d514171f17 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -14,12 +14,15 @@ #include "Arrange.hpp" #include "CustomGCode.hpp" #include "enum_bitmask.hpp" +//#include "ModelVolumeType.hpp" +#include "TextConfiguration.hpp" #include #include #include #include #include +#include namespace cereal { class BinaryInputArchive; @@ -765,6 +768,7 @@ public: void set_mesh(std::shared_ptr &mesh) { m_mesh = mesh; } void set_mesh(std::unique_ptr &&mesh) { m_mesh = std::move(mesh); } void reset_mesh() { m_mesh = std::make_shared(); } + const std::shared_ptr& get_mesh_shared_ptr() const { return m_mesh; } // Configuration parameters specific to an object model geometry or a modifier volume, // overriding the global Slic3r settings and the ModelObject settings. ModelConfigObject config; @@ -778,6 +782,10 @@ public: // List of mesh facets painted for MMU segmentation. FacetsAnnotation mmu_segmentation_facets; + // Is set only when volume is Embossed Text type + // Contain information how to re-create volume + std::optional text_configuration; + // A parent object owning this modifier volume. ModelObject* get_object() const { return this->object; } ModelVolumeType type() const { return m_type; } @@ -928,23 +936,8 @@ private: // 1 -> is splittable mutable int m_is_splittable{ -1 }; - ModelVolume(ModelObject *object, const TriangleMesh &mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART) : m_mesh(new TriangleMesh(mesh)), m_type(type), object(object) - { - assert(this->id().valid()); - assert(this->config.id().valid()); - assert(this->supported_facets.id().valid()); - assert(this->seam_facets.id().valid()); - assert(this->mmu_segmentation_facets.id().valid()); - assert(this->id() != this->config.id()); - assert(this->id() != this->supported_facets.id()); - assert(this->id() != this->seam_facets.id()); - assert(this->id() != this->mmu_segmentation_facets.id()); - if (mesh.facets_count() > 1) - calculate_convex_hull(); - } - ModelVolume(ModelObject *object, TriangleMesh &&mesh, TriangleMesh &&convex_hull, ModelVolumeType type = ModelVolumeType::MODEL_PART) : - m_mesh(new TriangleMesh(std::move(mesh))), m_convex_hull(new TriangleMesh(std::move(convex_hull))), m_type(type), object(object) { - assert(this->id().valid()); + inline bool check() { + assert(this->id().valid()); assert(this->config.id().valid()); assert(this->supported_facets.id().valid()); assert(this->seam_facets.id().valid()); @@ -953,6 +946,24 @@ private: assert(this->id() != this->supported_facets.id()); assert(this->id() != this->seam_facets.id()); assert(this->id() != this->mmu_segmentation_facets.id()); + return true; + } + + ModelVolume(ModelObject *object, const TriangleMesh &mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART) : + m_mesh(new TriangleMesh(mesh)), m_type(type), object(object) + { + assert(check()); + if (m_mesh->facets_count() > 1) calculate_convex_hull(); + } + ModelVolume(ModelObject *object, TriangleMesh &&mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART) + : m_mesh(new TriangleMesh(std::move(mesh))), m_type(type), object(object) + { + assert(check()); + if (m_mesh->facets_count() > 1) calculate_convex_hull(); + } + ModelVolume(ModelObject *object, TriangleMesh &&mesh, TriangleMesh &&convex_hull, ModelVolumeType type = ModelVolumeType::MODEL_PART) : + m_mesh(new TriangleMesh(std::move(mesh))), m_convex_hull(new TriangleMesh(std::move(convex_hull))), m_type(type), object(object) { + assert(check()); } // Copying an existing volume, therefore this volume will get a copy of the ID assigned. @@ -961,7 +972,8 @@ private: name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull), config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation), supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets), - cut_info(other.cut_info) + cut_info(other.cut_info), + text_configuration(other.text_configuration) { assert(this->id().valid()); assert(this->config.id().valid()); @@ -982,7 +994,8 @@ private: // Providing a new mesh, therefore this volume will get a new unique ID assigned. ModelVolume(ModelObject *object, const ModelVolume &other, TriangleMesh &&mesh) : name(other.name), source(other.source), config(other.config), object(object), m_mesh(new TriangleMesh(std::move(mesh))), m_type(other.m_type), m_transformation(other.m_transformation), - cut_info(other.cut_info) + cut_info(other.cut_info), + text_configuration(other.text_configuration) { assert(this->id().valid()); assert(this->config.id().valid()); @@ -1029,6 +1042,7 @@ private: cereal::load_by_value(ar, seam_facets); cereal::load_by_value(ar, mmu_segmentation_facets); cereal::load_by_value(ar, config); + cereal::load(ar, text_configuration); assert(m_mesh); if (has_convex_hull) { cereal::load_optional(ar, m_convex_hull); @@ -1045,6 +1059,7 @@ private: cereal::save_by_value(ar, seam_facets); cereal::save_by_value(ar, mmu_segmentation_facets); cereal::save_by_value(ar, config); + cereal::save(ar, text_configuration); if (has_convex_hull) cereal::save_optional(ar, m_convex_hull); } diff --git a/src/libslic3r/ModelVolumeType.hpp b/src/libslic3r/ModelVolumeType.hpp new file mode 100644 index 0000000000..5ae1c6440a --- /dev/null +++ b/src/libslic3r/ModelVolumeType.hpp @@ -0,0 +1,16 @@ +#ifndef slic3r_ModelVolumeType_hpp_ +#define slic3r_ModelVolumeType_hpp_ + +namespace Slic3r { + +enum class ModelVolumeType : int { + INVALID = -1, + MODEL_PART = 0, + NEGATIVE_VOLUME, + PARAMETER_MODIFIER, + SUPPORT_BLOCKER, + SUPPORT_ENFORCER, +}; + +} // namespace Slic3r +#endif /* slic3r_ModelVolumeType_hpp_ */ diff --git a/src/libslic3r/NSVGUtils.cpp b/src/libslic3r/NSVGUtils.cpp new file mode 100644 index 0000000000..a8f23e2e98 --- /dev/null +++ b/src/libslic3r/NSVGUtils.cpp @@ -0,0 +1,83 @@ +#include "NSVGUtils.hpp" +#include "ClipperUtils.hpp" + +using namespace Slic3r; + +void NSVGUtils::flatten_cubic_bez(Polygon &polygon, + float tessTol, + Vec2f p1, + Vec2f p2, + Vec2f p3, + Vec2f p4, + int level) +{ + Vec2f p12 = (p1 + p2) * 0.5f; + Vec2f p23 = (p2 + p3) * 0.5f; + Vec2f p34 = (p3 + p4) * 0.5f; + Vec2f p123 = (p12 + p23) * 0.5f; + + Vec2f pd = p4 - p1; + Vec2f pd2 = p2 - p4; + float d2 = std::abs(pd2.x() * pd.y() - pd2.y() * pd.x()); + Vec2f pd3 = p3 - p4; + float d3 = std::abs(pd3.x() * pd.y() - pd3.y() * pd.x()); + float d23 = d2 + d3; + + if ((d23 * d23) < tessTol * (pd.x() * pd.x() + pd.y() * pd.y())) { + polygon.points.emplace_back(p4.x(), p4.y()); + return; + } + + --level; + if (level == 0) return; + Vec2f p234 = (p23 + p34) * 0.5f; + Vec2f p1234 = (p123 + p234) * 0.5f; + flatten_cubic_bez(polygon, tessTol, p1, p12, p123, p1234, level); + flatten_cubic_bez(polygon, tessTol, p1234, p234, p34, p4, level); +} + +Polygons NSVGUtils::to_polygons(NSVGimage *image, float tessTol, int max_level) +{ + Polygons polygons; + for (NSVGshape *shape = image->shapes; shape != NULL; + shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE)) continue; + Slic3r::Polygon polygon; + if (shape->fill.type != NSVG_PAINT_NONE) { + for (NSVGpath *path = shape->paths; path != NULL; + path = path->next) { + // Flatten path + polygon.points.emplace_back(path->pts[0], path->pts[1]); + size_t path_size = (path->npts > 1) ? + static_cast(path->npts - 1) : 0; + for (size_t i = 0; i < path_size; i += 3) { + float *p = &path->pts[i * 2]; + Vec2f p1(p[0], p[1]), p2(p[2], p[3]), p3(p[4], p[5]), + p4(p[6], p[7]); + flatten_cubic_bez(polygon, tessTol, p1, p2, p3, p4, + max_level); + } + if (path->closed && !polygon.empty()) { + polygons.push_back(polygon); + polygon = Slic3r::Polygon(); + } + } + } + if (!polygon.empty()) + polygons.push_back(polygon); + } + return polygons; +} + +ExPolygons NSVGUtils::to_ExPolygons(NSVGimage *image, + float tessTol, + int max_level) +{ + Polygons polygons = to_polygons(image, tessTol, max_level); + + // Fix Y axis + for (Polygon &polygon : polygons) + for (Point &p : polygon.points) p.y() *= -1; + + return Slic3r::union_ex(polygons); +} \ No newline at end of file diff --git a/src/libslic3r/NSVGUtils.hpp b/src/libslic3r/NSVGUtils.hpp new file mode 100644 index 0000000000..d82901fcc8 --- /dev/null +++ b/src/libslic3r/NSVGUtils.hpp @@ -0,0 +1,34 @@ +#ifndef slic3r_NSVGUtils_hpp_ +#define slic3r_NSVGUtils_hpp_ + +#include "Polygon.hpp" +#include "ExPolygon.hpp" +#include "nanosvg/nanosvg.h" // load SVG file + +namespace Slic3r { + +// Helper function to work with nano svg +class NSVGUtils +{ +public: + NSVGUtils() = delete; + + // inspired by nanosvgrast.h function nsvgRasterize->nsvg__flattenShape + static void flatten_cubic_bez(Polygon &polygon, + float tessTol, + Vec2f p1, + Vec2f p2, + Vec2f p3, + Vec2f p4, + int level); + // convert svg image to ExPolygons + static ExPolygons to_ExPolygons(NSVGimage *image, + float tessTol = 10., + int max_level = 10); + // convert svg paths to Polygons + static Polygons to_polygons(NSVGimage *image, + float tessTol = 10., + int max_level = 10); +}; +} // namespace Slic3r +#endif // slic3r_NSVGUtils_hpp_ diff --git a/src/libslic3r/Point.cpp b/src/libslic3r/Point.cpp index a1145a952f..6a8fed8304 100644 --- a/src/libslic3r/Point.cpp +++ b/src/libslic3r/Point.cpp @@ -66,6 +66,24 @@ bool has_duplicate_points(std::vector &&pts) return false; } +Points collect_duplications(Points pts /* Copy */) +{ + std::stable_sort(pts.begin(), pts.end()); + Points duplicits; + const Point *prev = &pts.front(); + for (size_t i = 1; i < pts.size(); ++i) { + const Point *act = &pts[i]; + if (*prev == *act) { + // duplicit point + if (!duplicits.empty() && duplicits.back() == *act) + continue; // only unique duplicits + duplicits.push_back(*act); + } + prev = act; + } + return duplicits; +} + BoundingBox get_extents(const Points &pts) { return BoundingBox(pts); diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index b7ba3df63c..e3c61ff6b8 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -267,6 +267,9 @@ inline bool has_duplicate_successive_points_closed(const std::vector &pts return has_duplicate_successive_points(pts) || (pts.size() >= 2 && pts.front() == pts.back()); } +// Collect adjecent(duplicit points) +Points collect_duplications(Points pts /* Copy */); + inline bool shorter_then(const Point& p0, const coord_t len) { if (p0.x() > len || p0.x() < -len) @@ -547,6 +550,7 @@ namespace boost { namespace polygon { } } // end Boost +#include // Serialization through the Cereal library namespace cereal { // template void serialize(Archive& archive, Slic3r::Vec2crd &v) { archive(v.x(), v.y()); } @@ -560,10 +564,11 @@ namespace cereal { template void serialize(Archive& archive, Slic3r::Vec2d &v) { archive(v.x(), v.y()); } template void serialize(Archive& archive, Slic3r::Vec3d &v) { archive(v.x(), v.y(), v.z()); } - template void load(Archive& archive, Slic3r::Matrix2f &m) { archive.loadBinary((char*)m.data(), sizeof(float) * 4); } - template void save(Archive& archive, Slic3r::Matrix2f &m) { archive.saveBinary((char*)m.data(), sizeof(float) * 4); } - template void load(Archive& archive, Slic3r::Transform3d& m) { archive.loadBinary((char*)m.data(), sizeof(double) * 16); } - template void save(Archive& archive, const Slic3r::Transform3d& m) { archive.saveBinary((char*)m.data(), sizeof(double) * 16); } + template void serialize(Archive& archive, Slic3r::Matrix4d &m){ archive(binary_data(m.data(), 4*4*sizeof(double))); } + template void serialize(Archive& archive, Slic3r::Matrix2f &m){ archive(binary_data(m.data(), 2*2*sizeof(float))); } + + // Eigen Transformation serialization + template inline void serialize(Archive& archive, Eigen::Transform& t){ archive(t.matrix()); } } // To be able to use Vec<> and Mat<> in range based for loops: diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index b0d33db53f..3c4bb0e2ad 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -169,13 +169,16 @@ inline Points to_points(const Polygon &poly) return poly.points; } +inline size_t count_points(const Polygons &polys) { + size_t n_points = 0; + for (const auto &poly: polys) n_points += poly.points.size(); + return n_points; +} + inline Points to_points(const Polygons &polys) { - size_t n_points = 0; - for (size_t i = 0; i < polys.size(); ++ i) - n_points += polys[i].points.size(); Points points; - points.reserve(n_points); + points.reserve(count_points(polys)); for (const Polygon &poly : polys) append(points, poly.points); return points; @@ -195,11 +198,8 @@ inline Lines to_lines(const Polygon &poly) inline Lines to_lines(const Polygons &polys) { - size_t n_lines = 0; - for (size_t i = 0; i < polys.size(); ++ i) - n_lines += polys[i].points.size(); Lines lines; - lines.reserve(n_lines); + lines.reserve(count_points(polys)); for (size_t i = 0; i < polys.size(); ++ i) { const Polygon &poly = polys[i]; for (Points::const_iterator it = poly.points.begin(); it != poly.points.end()-1; ++it) diff --git a/src/libslic3r/QuadricEdgeCollapse.cpp b/src/libslic3r/QuadricEdgeCollapse.cpp index be0a90ec1a..b105b60756 100644 --- a/src/libslic3r/QuadricEdgeCollapse.cpp +++ b/src/libslic3r/QuadricEdgeCollapse.cpp @@ -184,7 +184,7 @@ void Slic3r::its_quadric_edge_collapse( throw_on_cancel(); status_fn(status_init_size); - //its_store_triangle(its, "triangle.obj", 1182); + //its_store_triangle_to_obj(its, "triangle.obj", 1182); //store_surround("triangle_surround1.obj", 1182, 1, its, v_infos, e_infos); // convert from triangle index to mutable priority queue index @@ -904,7 +904,7 @@ void QuadricEdgeCollapse::store_surround(const char *obj_filename, std::vector trs; trs.reserve(triangles.size()); for (size_t ti : triangles) trs.push_back(ti); - its_store_triangles(its, obj_filename, trs); + its_store_triangles_to_obj(its, obj_filename, trs); // its_write_obj(its,"original.obj"); } diff --git a/src/libslic3r/QuadricEdgeCollapse.hpp b/src/libslic3r/QuadricEdgeCollapse.hpp index c7330bfc5f..956ad35113 100644 --- a/src/libslic3r/QuadricEdgeCollapse.hpp +++ b/src/libslic3r/QuadricEdgeCollapse.hpp @@ -1,3 +1,6 @@ +#ifndef slic3r_quadric_edge_collapse_hpp_ +#define slic3r_quadric_edge_collapse_hpp_ + // paper: https://people.eecs.berkeley.edu/~jrs/meshpapers/GarlandHeckbert2.pdf // sum up: https://users.csc.calpoly.edu/~zwood/teaching/csc570/final06/jseeba/ // inspiration: https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification @@ -26,3 +29,4 @@ void its_quadric_edge_collapse( std::function statusfn = nullptr); } // namespace Slic3r +#endif // slic3r_quadric_edge_collapse_hpp_ diff --git a/src/libslic3r/SVG.cpp b/src/libslic3r/SVG.cpp index 03c8823d34..d4088089bc 100644 --- a/src/libslic3r/SVG.cpp +++ b/src/libslic3r/SVG.cpp @@ -378,4 +378,10 @@ void SVG::export_expolygons(const char *path, const std::vector(x) * 10.f; } + +} // namespace Slic3r diff --git a/src/libslic3r/SVG.hpp b/src/libslic3r/SVG.hpp index 3ff2213d81..25727139e2 100644 --- a/src/libslic3r/SVG.hpp +++ b/src/libslic3r/SVG.hpp @@ -167,9 +167,9 @@ public: { export_expolygons(path.c_str(), expolygons_with_attributes); } private: - static float to_svg_coord(float x) throw() { return unscale(x) * 10.f; } - static float to_svg_x(float x) throw() { return to_svg_coord(x); } - float to_svg_y(float x) const throw() { return flipY ? this->height - to_svg_coord(x) : to_svg_coord(x); } + static float to_svg_coord(float x) throw(); + static float to_svg_x(float x) throw() { return to_svg_coord(x); } + float to_svg_y(float x) const throw() { return flipY ? this->height - to_svg_coord(x) : to_svg_coord(x); } }; } diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index a3e72cf0c5..c80689f48b 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -1,69 +1,69 @@ -#ifndef _prusaslicer_technologies_h_ -#define _prusaslicer_technologies_h_ - -//============= -// debug techs -//============= -// Shows camera target in the 3D scene -#define ENABLE_SHOW_CAMERA_TARGET 0 -// Log debug messages to console when changing selection -#define ENABLE_SELECTION_DEBUG_OUTPUT 0 -// Renders a small sphere in the center of the bounding box of the current selection when no gizmo is active -#define ENABLE_RENDER_SELECTION_CENTER 0 -// Shows an imgui dialog with camera related data -#define ENABLE_CAMERA_STATISTICS 0 -// Enable extracting thumbnails from selected gcode and save them as png files -#define ENABLE_THUMBNAIL_GENERATOR_DEBUG 0 -// Disable synchronization of unselected instances -#define DISABLE_INSTANCES_SYNCH 0 -// Use wxDataViewRender instead of wxDataViewCustomRenderer -#define ENABLE_NONCUSTOM_DATA_VIEW_RENDERING 0 -// Enable G-Code viewer statistics imgui dialog -#define ENABLE_GCODE_VIEWER_STATISTICS 0 -// Enable G-Code viewer comparison between toolpaths height and width detected from gcode and calculated at gcode generation -#define ENABLE_GCODE_VIEWER_DATA_CHECKING 0 -// Enable project dirty state manager debug window -#define ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW 0 -// Disable using instanced models to render options in gcode preview -#define DISABLE_GCODEVIEWER_INSTANCED_MODELS 1 -// Enable Measure Gizmo debug window -#define ENABLE_MEASURE_GIZMO_DEBUG 0 - - -// Enable rendering of objects using environment map -#define ENABLE_ENVIRONMENT_MAP 0 -// Enable smoothing of objects normals -#define ENABLE_SMOOTH_NORMALS 0 - - -//==================== -// 2.5.0.alpha1 techs -//==================== -#define ENABLE_2_5_0_ALPHA1 1 - -// Enable removal of legacy OpenGL calls -#define ENABLE_LEGACY_OPENGL_REMOVAL (1 && ENABLE_2_5_0_ALPHA1) -// Enable OpenGL ES -#define ENABLE_OPENGL_ES (0 && ENABLE_LEGACY_OPENGL_REMOVAL) -// Enable OpenGL core profile context (tested against Mesa 20.1.8 on Windows) -#define ENABLE_GL_CORE_PROFILE (1 && ENABLE_LEGACY_OPENGL_REMOVAL && !ENABLE_OPENGL_ES) -// Enable OpenGL debug messages using debug context -#define ENABLE_OPENGL_DEBUG_OPTION (1 && ENABLE_GL_CORE_PROFILE) -// Shows an imgui dialog with GLModel statistics data -#define ENABLE_GLMODEL_STATISTICS (0 && ENABLE_LEGACY_OPENGL_REMOVAL) -// Enable rework of Reload from disk command -#define ENABLE_RELOAD_FROM_DISK_REWORK (1 && ENABLE_2_5_0_ALPHA1) -// Enable editing volumes transformation in world coordinates and instances in local coordinates -#define ENABLE_WORLD_COORDINATE (1 && ENABLE_2_5_0_ALPHA1) -// Enable alternative version of file_wildcards() -#define ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR (1 && ENABLE_2_5_0_ALPHA1) -// Enable processing of gcode G2 and G3 lines -#define ENABLE_PROCESS_G2_G3_LINES (1 && ENABLE_2_5_0_ALPHA1) -// Enable fix of used filament data exported to gcode file -#define ENABLE_USED_FILAMENT_POST_PROCESS (1 && ENABLE_2_5_0_ALPHA1) -// Enable picking using raytracing -#define ENABLE_RAYCAST_PICKING (1 && ENABLE_LEGACY_OPENGL_REMOVAL) -#define ENABLE_RAYCAST_PICKING_DEBUG (0 && ENABLE_RAYCAST_PICKING) - - -#endif // _prusaslicer_technologies_h_ +#ifndef _prusaslicer_technologies_h_ +#define _prusaslicer_technologies_h_ + +//============= +// debug techs +//============= +// Shows camera target in the 3D scene +#define ENABLE_SHOW_CAMERA_TARGET 0 +// Log debug messages to console when changing selection +#define ENABLE_SELECTION_DEBUG_OUTPUT 0 +// Renders a small sphere in the center of the bounding box of the current selection when no gizmo is active +#define ENABLE_RENDER_SELECTION_CENTER 0 +// Shows an imgui dialog with camera related data +#define ENABLE_CAMERA_STATISTICS 0 +// Enable extracting thumbnails from selected gcode and save them as png files +#define ENABLE_THUMBNAIL_GENERATOR_DEBUG 0 +// Disable synchronization of unselected instances +#define DISABLE_INSTANCES_SYNCH 0 +// Use wxDataViewRender instead of wxDataViewCustomRenderer +#define ENABLE_NONCUSTOM_DATA_VIEW_RENDERING 0 +// Enable G-Code viewer statistics imgui dialog +#define ENABLE_GCODE_VIEWER_STATISTICS 0 +// Enable G-Code viewer comparison between toolpaths height and width detected from gcode and calculated at gcode generation +#define ENABLE_GCODE_VIEWER_DATA_CHECKING 0 +// Enable project dirty state manager debug window +#define ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW 0 +// Disable using instanced models to render options in gcode preview +#define DISABLE_GCODEVIEWER_INSTANCED_MODELS 1 +// Enable Measure Gizmo debug window +#define ENABLE_MEASURE_GIZMO_DEBUG 0 + + +// Enable rendering of objects using environment map +#define ENABLE_ENVIRONMENT_MAP 0 +// Enable smoothing of objects normals +#define ENABLE_SMOOTH_NORMALS 0 + + +//==================== +// 2.5.0.alpha1 techs +//==================== +#define ENABLE_2_5_0_ALPHA1 1 + +// Enable removal of legacy OpenGL calls +#define ENABLE_LEGACY_OPENGL_REMOVAL (1 && ENABLE_2_5_0_ALPHA1) +// Enable OpenGL ES +#define ENABLE_OPENGL_ES (0 && ENABLE_LEGACY_OPENGL_REMOVAL) +// Enable OpenGL core profile context (tested against Mesa 20.1.8 on Windows) +#define ENABLE_GL_CORE_PROFILE (1 && ENABLE_LEGACY_OPENGL_REMOVAL && !ENABLE_OPENGL_ES) +// Enable OpenGL debug messages using debug context +#define ENABLE_OPENGL_DEBUG_OPTION (1 && ENABLE_GL_CORE_PROFILE) +// Shows an imgui dialog with GLModel statistics data +#define ENABLE_GLMODEL_STATISTICS (0 && ENABLE_LEGACY_OPENGL_REMOVAL) +// Enable rework of Reload from disk command +#define ENABLE_RELOAD_FROM_DISK_REWORK (1 && ENABLE_2_5_0_ALPHA1) +// Enable editing volumes transformation in world coordinates and instances in local coordinates +#define ENABLE_WORLD_COORDINATE (1 && ENABLE_2_5_0_ALPHA1) +// Enable alternative version of file_wildcards() +#define ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR (1 && ENABLE_2_5_0_ALPHA1) +// Enable processing of gcode G2 and G3 lines +#define ENABLE_PROCESS_G2_G3_LINES (1 && ENABLE_2_5_0_ALPHA1) +// Enable fix of used filament data exported to gcode file +#define ENABLE_USED_FILAMENT_POST_PROCESS (1 && ENABLE_2_5_0_ALPHA1) +// Enable picking using raytracing +#define ENABLE_RAYCAST_PICKING (1 && ENABLE_LEGACY_OPENGL_REMOVAL) +#define ENABLE_RAYCAST_PICKING_DEBUG (0 && ENABLE_RAYCAST_PICKING) + + +#endif // _prusaslicer_technologies_h_ diff --git a/src/libslic3r/TextConfiguration.hpp b/src/libslic3r/TextConfiguration.hpp new file mode 100644 index 0000000000..f303b17e50 --- /dev/null +++ b/src/libslic3r/TextConfiguration.hpp @@ -0,0 +1,239 @@ +#ifndef slic3r_TextConfiguration_hpp_ +#define slic3r_TextConfiguration_hpp_ + +#include +#include +#include +#include +#include +#include +#include +#include "Point.hpp" // Transform3d + +namespace Slic3r { + +/// +/// User modifiable property of text style +/// NOTE: OnEdit fix serializations: EmbossStylesSerializable, TextConfigurationSerialization +/// +struct FontProp +{ + // define extra space between letters, negative mean closer letter + // When not set value is zero and is not stored + std::optional char_gap; // [in font point] + + // define extra space between lines, negative mean closer lines + // When not set value is zero and is not stored + std::optional line_gap; // [in font point] + + // Z depth of text + float emboss; // [in mm] + + // Flag that text should use surface cutted from object + // FontProp::distance should without value + // FontProp::emboss should be positive number + // Note: default value is false + bool use_surface; + + // positive value mean wider character shape + // negative value mean tiner character shape + // When not set value is zero and is not stored + std::optional boldness; // [in mm] + + // positive value mean italic of character (CW) + // negative value mean CCW skew (unItalic) + // When not set value is zero and is not stored + std::optional skew; // [ration x:y] + + // distance from surface point + // used for move over model surface + // When not set value is zero and is not stored + std::optional distance; // [in mm] + + // change up vector direction of font + // When not set value is zero and is not stored + std::optional angle; // [in radians] + + // Parameter for True Type Font collections + // Select index of font in collection + std::optional collection_number; + + //enum class Align { + // left, + // right, + // center, + // top_left, + // top_right, + // top_center, + // bottom_left, + // bottom_right, + // bottom_center + //}; + //// change pivot of text + //// When not set, center is used and is not stored + //std::optional align; + + ////// + // Duplicit data to wxFontDescriptor + // used for store/load .3mf file + ////// + + // Height of text line (letters) + // duplicit to wxFont::PointSize + float size_in_mm; // [in mm] + + // Additional data about font to be able to find substitution, + // when same font is not installed + std::optional family; + std::optional face_name; + std::optional style; + std::optional weight; + + /// + /// Only constructor with restricted values + /// + /// Y size of text [in mm] + /// Z size of text [in mm] + FontProp(float line_height = 10.f, float depth = 2.f) + : emboss(depth), size_in_mm(line_height), use_surface(false) + {} + + bool operator==(const FontProp& other) const { + return + char_gap == other.char_gap && + line_gap == other.line_gap && + use_surface == other.use_surface && + is_approx(emboss, other.emboss) && + is_approx(size_in_mm, other.size_in_mm) && + is_approx(boldness, other.boldness) && + is_approx(skew, other.skew) && + is_approx(distance, other.distance) && + is_approx(angle, other.angle); + } + + // undo / redo stack recovery + template void save(Archive &ar) const + { + ar(emboss, use_surface, size_in_mm); + cereal::save(ar, char_gap); + cereal::save(ar, line_gap); + cereal::save(ar, boldness); + cereal::save(ar, skew); + cereal::save(ar, distance); + cereal::save(ar, angle); + cereal::save(ar, collection_number); + cereal::save(ar, family); + cereal::save(ar, face_name); + cereal::save(ar, style); + cereal::save(ar, weight); + } + template void load(Archive &ar) + { + ar(emboss, use_surface, size_in_mm); + cereal::load(ar, char_gap); + cereal::load(ar, line_gap); + cereal::load(ar, boldness); + cereal::load(ar, skew); + cereal::load(ar, distance); + cereal::load(ar, angle); + cereal::load(ar, collection_number); + cereal::load(ar, family); + cereal::load(ar, face_name); + cereal::load(ar, style); + cereal::load(ar, weight); + } +}; + +/// +/// Style of embossed text +/// (Path + Type) must define how to open font for using on different OS +/// NOTE: OnEdit fix serializations: EmbossStylesSerializable, TextConfigurationSerialization +/// +struct EmbossStyle +{ + // Human readable name of style it is shown in GUI + std::string name; + + // Define how to open font + // Meaning depend on type + std::string path; + + enum class Type; + // Define what is stored in path + Type type { Type::undefined }; + + // User modification of font style + FontProp prop; + + // when name is empty than Font item was loaded from .3mf file + // and potentionaly it is not reproducable + // define data stored in path + // when wx change way of storing add new descriptor Type + enum class Type { + undefined = 0, + + // wx font descriptors are platform dependent + // path is font descriptor generated by wxWidgets + wx_win_font_descr, // on Windows + wx_lin_font_descr, // on Linux + wx_mac_font_descr, // on Max OS + + // TrueTypeFont file loacation on computer + // for privacy: only filename is stored into .3mf + file_path + }; + + bool operator==(const EmbossStyle &other) const + { + return + type == other.type && + prop == other.prop && + name == other.name && + path == other.path + ; + } + + // undo / redo stack recovery + template void serialize(Archive &ar){ + ar(name, path, type, prop); + } +}; + +// Emboss style name inside vector is unique +// It is not map beacuse items has own order (view inside of slect) +// It is stored into AppConfig by EmbossStylesSerializable +using EmbossStyles = std::vector; + +/// +/// Define how to create 'Text volume' +/// It is stored into .3mf by TextConfigurationSerialization +/// It is part of ModelVolume optional data +/// +struct TextConfiguration +{ + // Style of embossed text + EmbossStyle style; + + // Embossed text value + std::string text = "None"; + + // !!! Volume stored in .3mf has transformed vertices. + // (baked transformation into vertices position) + // Only place for fill this is when load from .3mf + // This is correct volume transformation + std::optional fix_3mf_tr; + + // undo / redo stack recovery + template void save(Archive &ar) const{ + ar(text, style); + cereal::save(ar, fix_3mf_tr); + } + template void load(Archive &ar){ + ar(text, style); + cereal::load(ar, fix_3mf_tr); + } +}; + +} // namespace Slic3r + +#endif // slic3r_TextConfiguration_hpp_ diff --git a/src/libslic3r/Timer.cpp b/src/libslic3r/Timer.cpp new file mode 100644 index 0000000000..b361427a65 --- /dev/null +++ b/src/libslic3r/Timer.cpp @@ -0,0 +1,12 @@ +#include "Timer.hpp" +#include + +using namespace std::chrono; + +Slic3r::Timer::Timer(const std::string &name) : m_name(name), m_start(steady_clock::now()) {} + +Slic3r::Timer::~Timer() +{ + BOOST_LOG_TRIVIAL(debug) << "Timer '" << m_name << "' spend " << + duration_cast(steady_clock::now() - m_start).count() << "ms"; +} diff --git a/src/libslic3r/Timer.hpp b/src/libslic3r/Timer.hpp new file mode 100644 index 0000000000..b8f9736a17 --- /dev/null +++ b/src/libslic3r/Timer.hpp @@ -0,0 +1,31 @@ +#ifndef libslic3r_Timer_hpp_ +#define libslic3r_Timer_hpp_ + +#include +#include + +namespace Slic3r { + +/// +/// Instance of this class is used for measure time consumtion +/// of block code until instance is alive and write result to debug output +/// +class Timer +{ + std::string m_name; + std::chrono::steady_clock::time_point m_start; +public: + /// + /// name describe timer + /// + /// Describe timer in consol log + Timer(const std::string& name); + + /// + /// name describe timer + /// + ~Timer(); +}; + +} // namespace Slic3r +#endif // libslic3r_Timer_hpp_ \ No newline at end of file diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index ad7e1a2bb0..5610c9f780 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -783,9 +783,9 @@ int its_compactify_vertices(indexed_triangle_set &its, bool shrink_to_fit) return removed; } -bool its_store_triangle(const indexed_triangle_set &its, - const char * obj_filename, - size_t triangle_index) +bool its_store_triangle_to_obj(const indexed_triangle_set &its, + const char *obj_filename, + size_t triangle_index) { if (its.indices.size() <= triangle_index) return false; Vec3i t = its.indices[triangle_index]; @@ -796,9 +796,9 @@ bool its_store_triangle(const indexed_triangle_set &its, return its_write_obj(its2, obj_filename); } -bool its_store_triangles(const indexed_triangle_set &its, - const char * obj_filename, - const std::vector & triangles) +bool its_store_triangles_to_obj(const indexed_triangle_set &its, + const char *obj_filename, + const std::vector &triangles) { indexed_triangle_set its2; its2.vertices.reserve(triangles.size() * 3); @@ -1206,6 +1206,23 @@ void its_reverse_all_facets(indexed_triangle_set &its) std::swap(face[0], face[1]); } +void its_merge(indexed_triangle_set &its, indexed_triangle_set &&its_add) +{ + if (its.empty()) { + its = std::move(its_add); + return; + } + auto &verts = its.vertices; + size_t verts_size = verts.size(); + Slic3r::append(verts, std::move(its_add.vertices)); + + // increase face indices + int offset = static_cast(verts_size); + for (auto &face : its_add.indices) + for (int i = 0; i < 3; ++i) face[i] += offset; + Slic3r::append(its.indices, std::move(its_add.indices)); +} + void its_merge(indexed_triangle_set &A, const indexed_triangle_set &B) { auto N = int(A.vertices.size()); diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index 08595a2d3b..c288b6d3a6 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -211,8 +211,8 @@ int its_remove_degenerate_faces(indexed_triangle_set &its, bool shrink_to_fit = int its_compactify_vertices(indexed_triangle_set &its, bool shrink_to_fit = true); // store part of index triangle set -bool its_store_triangle(const indexed_triangle_set &its, const char *obj_filename, size_t triangle_index); -bool its_store_triangles(const indexed_triangle_set &its, const char *obj_filename, const std::vector& triangles); +bool its_store_triangle_to_obj(const indexed_triangle_set &its, const char *obj_filename, size_t triangle_index); +bool its_store_triangles_to_obj(const indexed_triangle_set &its, const char *obj_filename, const std::vector& triangles); std::vector its_split(const indexed_triangle_set &its); std::vector its_split(const indexed_triangle_set &its, std::vector &face_neighbors); @@ -285,6 +285,14 @@ inline stl_normal its_unnormalized_normal(const indexed_triangle_set &its, float its_volume(const indexed_triangle_set &its); float its_average_edge_length(const indexed_triangle_set &its); +/// +/// Merge one triangle mesh to another +/// Added triangle set will be consumed +/// +/// IN/OUT triangle mesh +/// Triangle mesh (will be consumed) +void its_merge(indexed_triangle_set &its, indexed_triangle_set &&its_add); + void its_merge(indexed_triangle_set &A, const indexed_triangle_set &B); void its_merge(indexed_triangle_set &A, const std::vector &triangles); void its_merge(indexed_triangle_set &A, const Pointf3s &triangles); diff --git a/src/libslic3r/Triangulation.cpp b/src/libslic3r/Triangulation.cpp new file mode 100644 index 0000000000..a355d725d2 --- /dev/null +++ b/src/libslic3r/Triangulation.cpp @@ -0,0 +1,328 @@ +#include "Triangulation.hpp" +#include "IntersectionPoints.hpp" +#include +#include +#include +#include + +using namespace Slic3r; +namespace priv{ +inline void insert_edges(Triangulation::HalfEdges &edges, uint32_t &offset, const Polygon &polygon, const Triangulation::Changes& changes) { + const Points &pts = polygon.points; + uint32_t size = static_cast(pts.size()); + uint32_t last_index = offset + size - 1; + uint32_t prev_index = changes[last_index]; + for (uint32_t i = 0; i < size; ++i) { + uint32_t index = changes[offset + i]; + // when duplicit points are neighbor + if (prev_index == index) continue; + edges.push_back({prev_index, index}); + prev_index = index; + } + offset += size; +} + +inline void insert_edges(Triangulation::HalfEdges &edges, uint32_t &offset, const Polygon &polygon) { + const Points &pts = polygon.points; + uint32_t size = static_cast(pts.size()); + uint32_t prev_index = offset + size - 1; + for (uint32_t i = 0; i < size; ++i) { + uint32_t index = offset + i; + edges.push_back({prev_index, index}); + prev_index = index; + } + offset += size; +} + +inline bool has_bidirectional_constrained( + const Triangulation::HalfEdges &constrained) +{ + for (const auto &c : constrained) { + auto key = std::make_pair(c.second, c.first); + auto it = std::lower_bound(constrained.begin(), constrained.end(), + key); + if (it != constrained.end() && *it == key) return true; + } + return false; +} + +inline bool is_unique(const Points &points) { + Points pts = points; // copy + std::sort(pts.begin(), pts.end()); + auto it = std::adjacent_find(pts.begin(), pts.end()); + return it == pts.end(); +} + +inline bool has_self_intersection( + const Points &points, + const Triangulation::HalfEdges &constrained_half_edges) +{ + Lines lines; + lines.reserve(constrained_half_edges.size()); + for (const auto &he : constrained_half_edges) + lines.emplace_back(points[he.first], points[he.second]); + return !intersection_points(lines).empty(); +} + +} // namespace priv + +//#define VISUALIZE_TRIANGULATION +#ifdef VISUALIZE_TRIANGULATION +#include "admesh/stl.h" // indexed triangle set +static void visualize(const Points &points, + const Triangulation::Indices &indices, + const char *filename) +{ + // visualize + indexed_triangle_set its; + its.vertices.reserve(points.size()); + for (const Point &p : points) its.vertices.emplace_back(p.x(), p.y(), 0.); + its.indices = indices; + its_write_obj(its, filename); +} +#endif // VISUALIZE_TRIANGULATION + +Triangulation::Indices Triangulation::triangulate(const Points &points, + const HalfEdges &constrained_half_edges) +{ + assert(!points.empty()); + assert(!constrained_half_edges.empty()); + // constrained must be sorted + assert(std::is_sorted(constrained_half_edges.begin(), + constrained_half_edges.end())); + // check that there is no duplicit constrained edge + assert(std::adjacent_find(constrained_half_edges.begin(), constrained_half_edges.end()) == constrained_half_edges.end()); + // edges can NOT contain bidirectional constrained + assert(!priv::has_bidirectional_constrained(constrained_half_edges)); + // check that there is only unique poistion of points + assert(priv::is_unique(points)); + assert(!priv::has_self_intersection(points, constrained_half_edges)); + // use cgal triangulation + using K = CGAL::Exact_predicates_inexact_constructions_kernel; + using Vb = CGAL::Triangulation_vertex_base_with_info_2; + using Fb = CGAL::Constrained_triangulation_face_base_2; + using Tds = CGAL::Triangulation_data_structure_2; + using CDT = CGAL::Constrained_Delaunay_triangulation_2; + + // construct a constrained triangulation + CDT cdt; + { + std::vector vertices_handle(points.size()); // for constriants + using Point_with_ord = std::pair; + using SearchTrait = CGAL::Spatial_sort_traits_adapter_2 + >; + + std::vector cdt_points; + cdt_points.reserve(points.size()); + size_t ord = 0; + for (const auto &p : points) + cdt_points.emplace_back(std::make_pair(CDT::Point{p.x(), p.y()}, ord++)); + + SearchTrait st; + CGAL::spatial_sort(cdt_points.begin(), cdt_points.end(), st); + CDT::Face_handle f; + for (const auto& p : cdt_points) { + auto handle = cdt.insert(p.first, f); + handle->info() = p.second; + vertices_handle[p.second] = handle; + f = handle->face(); + } + + // Constrain the triangulation. + for (const HalfEdge &edge : constrained_half_edges) + cdt.insert_constraint(vertices_handle[edge.first], vertices_handle[edge.second]); + } + + auto faces = cdt.finite_face_handles(); + + // Unmark constrained edges of outside faces. + size_t num_faces = 0; + for (CDT::Face_handle fh : faces) { + for (int i = 0; i < 3; ++i) { + if (!fh->is_constrained(i)) continue; + auto key = std::make_pair(fh->vertex((i + 2) % 3)->info(), fh->vertex((i + 1) % 3)->info()); + auto it = std::lower_bound(constrained_half_edges.begin(), constrained_half_edges.end(), key); + if (it == constrained_half_edges.end() || *it != key) continue; + // This face contains a constrained edge and it is outside. + for (int j = 0; j < 3; ++ j) + fh->set_constraint(j, false); + --num_faces; + break; + } + ++num_faces; + } + + auto inside = [](CDT::Face_handle &fh) { + return fh->neighbor(0) != fh && + (fh->is_constrained(0) || + fh->is_constrained(1) || + fh->is_constrained(2)); + }; + +#ifdef VISUALIZE_TRIANGULATION + std::vector indices2; + indices2.reserve(num_faces); + for (CDT::Face_handle fh : faces) + if (inside(fh)) indices2.emplace_back(fh->vertex(0)->info(), fh->vertex(1)->info(), fh->vertex(2)->info()); + visualize(points, indices2, "C:/data/temp/triangulation_without_floodfill.obj"); +#endif // VISUALIZE_TRIANGULATION + + // Propagate inside the constrained regions. + std::vector queue; + queue.reserve(num_faces); + for (CDT::Face_handle seed : faces){ + if (!inside(seed)) continue; + // Seed fill to neighbor faces. + queue.emplace_back(seed); + while (! queue.empty()) { + CDT::Face_handle fh = queue.back(); + queue.pop_back(); + for (int i = 0; i < 3; ++i) { + if (fh->is_constrained(i)) continue; + // Propagate along this edge. + fh->set_constraint(i, true); + CDT::Face_handle nh = fh->neighbor(i); + bool was_inside = inside(nh); + // Mark the other side of this edge. + nh->set_constraint(nh->index(fh), true); + if (! was_inside) + queue.push_back(nh); + } + } + } + + std::vector indices; + indices.reserve(num_faces); + for (CDT::Face_handle fh : faces) + if (inside(fh)) + indices.emplace_back(fh->vertex(0)->info(), fh->vertex(1)->info(), fh->vertex(2)->info()); + +#ifdef VISUALIZE_TRIANGULATION + visualize(points, indices, "C:/data/temp/triangulation.obj"); +#endif // VISUALIZE_TRIANGULATION + + return indices; +} + +Triangulation::Indices Triangulation::triangulate(const Polygon &polygon) +{ + const Points &pts = polygon.points; + HalfEdges edges; + edges.reserve(pts.size()); + uint32_t offset = 0; + priv::insert_edges(edges, offset, polygon); + std::sort(edges.begin(), edges.end()); + return triangulate(pts, edges); +} + +Triangulation::Indices Triangulation::triangulate(const Polygons &polygons) +{ + size_t count = count_points(polygons); + Points points; + points.reserve(count); + + HalfEdges edges; + edges.reserve(count); + uint32_t offset = 0; + + for (const Polygon &polygon : polygons) { + Slic3r::append(points, polygon.points); + priv::insert_edges(edges, offset, polygon); + } + + std::sort(edges.begin(), edges.end()); + return triangulate(points, edges); +} + +Triangulation::Indices Triangulation::triangulate(const ExPolygon &expolygon){ + ExPolygons expolys({expolygon}); + return triangulate(expolys); +} + +Triangulation::Indices Triangulation::triangulate(const ExPolygons &expolygons){ + Points pts = to_points(expolygons); + Points d_pts = collect_duplications(pts); + if (d_pts.empty()) return triangulate(expolygons, pts); + + Changes changes = create_changes(pts, d_pts); + Indices indices = triangulate(expolygons, pts, changes); + // reverse map for changes + Changes changes2(changes.size(), std::numeric_limits::max()); + for (size_t i = 0; i < changes.size(); ++i) + changes2[changes[i]] = i; + + // convert indices into expolygons indicies + for (Vec3i &t : indices) + for (size_t ti = 0; ti < 3; ti++) t[ti] = changes2[t[ti]]; + + return indices; +} + +Triangulation::Indices Triangulation::triangulate(const ExPolygons &expolygons, const Points &points) +{ + assert(count_points(expolygons) == points.size()); + // when contain duplicit coordinate in points will not work properly + assert(collect_duplications(points).empty()); + + HalfEdges edges; + edges.reserve(points.size()); + uint32_t offset = 0; + for (const ExPolygon &expolygon : expolygons) { + priv::insert_edges(edges, offset, expolygon.contour); + for (const Polygon &hole : expolygon.holes) + priv::insert_edges(edges, offset, hole); + } + std::sort(edges.begin(), edges.end()); + return triangulate(points, edges); +} + +Triangulation::Indices Triangulation::triangulate(const ExPolygons &expolygons, const Points& points, const Changes& changes) +{ + assert(!points.empty()); + assert(count_points(expolygons) == points.size()); + assert(changes.size() == points.size()); + // IMPROVE: search from end and somehow distiquish that value is not a change + uint32_t count_points = *std::max_element(changes.begin(), changes.end())+1; + Points pts(count_points); + for (size_t i = 0; i < changes.size(); i++) + pts[changes[i]] = points[i]; + + HalfEdges edges; + edges.reserve(points.size()); + uint32_t offset = 0; + for (const ExPolygon &expolygon : expolygons) { + priv::insert_edges(edges, offset, expolygon.contour, changes); + for (const Polygon &hole : expolygon.holes) + priv::insert_edges(edges, offset, hole, changes); + } + + std::sort(edges.begin(), edges.end()); + return triangulate(pts, edges); +} + +Triangulation::Changes Triangulation::create_changes(const Points &points, const Points &duplicits) +{ + assert(!duplicits.empty()); + assert(duplicits.size() < points.size()/2); + std::vector duplicit_indices(duplicits.size(), std::numeric_limits::max()); + Changes changes; + changes.reserve(points.size()); + uint32_t index = 0; + for (const Point &p: points) { + auto it = std::lower_bound(duplicits.begin(), duplicits.end(), p); + if (it == duplicits.end() || *it != p) { + changes.push_back(index); + ++index; + continue; + } + uint32_t &d_index = duplicit_indices[it - duplicits.begin()]; + if (d_index == std::numeric_limits::max()) { + d_index = index; + changes.push_back(index); + ++index; + } else { + changes.push_back(d_index); + } + } + return changes; +} diff --git a/src/libslic3r/Triangulation.hpp b/src/libslic3r/Triangulation.hpp new file mode 100644 index 0000000000..2a6ff811d2 --- /dev/null +++ b/src/libslic3r/Triangulation.hpp @@ -0,0 +1,72 @@ +#ifndef libslic3r_Triangulation_hpp_ +#define libslic3r_Triangulation_hpp_ + +#include +#include +#include +#include +#include + +namespace Slic3r { + +class Triangulation +{ +public: + Triangulation() = delete; + + // define oriented connection of 2 vertices(defined by its index) + using HalfEdge = std::pair; + using HalfEdges = std::vector; + using Indices = std::vector; + + /// + /// Connect points by triangulation to create filled surface by triangles + /// Input points have to be unique + /// Inspiration for make unique points is Emboss::dilate_to_unique_points + /// + /// Points to connect + /// Constraint for edges, pair is from point(first) to + /// point(second), sorted lexicographically + /// Triangles + static Indices triangulate(const Points &points, + const HalfEdges &half_edges); + static Indices triangulate(const Polygon &polygon); + static Indices triangulate(const Polygons &polygons); + static Indices triangulate(const ExPolygon &expolygon); + static Indices triangulate(const ExPolygons &expolygons); + + // Map for convert original index to set without duplication + // from_index + using Changes = std::vector; + + /// + /// Create conversion map from original index into new + /// with respect of duplicit point + /// + /// input set of points + /// duplicit points collected from points + /// Conversion map for point index + static Changes create_changes(const Points &points, const Points &duplicits); + + /// + /// Triangulation for expolygons, speed up when points are already collected + /// NOTE: Not working properly for ExPolygons with multiple point on same coordinate + /// You should check it by "collect_changes" + /// + /// Input shape to triangulation - define edges + /// Points from expolygons + /// Triangle indices + static Indices triangulate(const ExPolygons &expolygons, const Points& points); + + /// + /// Triangulation for expolygons containing multiple points with same coordinate + /// + /// Input shape to triangulation - define edge + /// Points from expolygons + /// Changes swap for indicies into points + /// Triangle indices + static Indices triangulate(const ExPolygons &expolygons, const Points& points, const Changes& changes); +}; + +} // namespace Slic3r +#endif // libslic3r_Triangulation_hpp_ \ No newline at end of file diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 73bb418afb..495bcbc9b7 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -237,6 +237,7 @@ inline typename CONTAINER_TYPE::value_type& next_value_modulo(typename CONTAINER } extern std::string xml_escape(std::string text, bool is_marked = false); +extern std::string xml_escape_double_quotes_attribute_value(std::string text); #if defined __GNUC__ && __GNUC__ < 5 && !defined __clang__ diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index ac889cc730..1afd368aae 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -21,6 +21,7 @@ #include #include #include +#include #ifdef _WIN32 // On MSVC, std::deque degenerates to a list of pointers, which defeats its purpose of reducing allocator load and memory fragmentation. @@ -109,9 +110,10 @@ template inline void append(std::vector& dest, const std::vector& src) { if (dest.empty()) - dest = src; + dest = src; // copy else dest.insert(dest.end(), src.begin(), src.end()); + // NOTE: insert reserve space when needed } template @@ -120,11 +122,14 @@ inline void append(std::vector& dest, std::vector&& src) if (dest.empty()) dest = std::move(src); else { - dest.reserve(dest.size() + src.size()); - std::move(std::begin(src), std::end(src), std::back_inserter(dest)); + dest.insert(dest.end(), + std::make_move_iterator(src.begin()), + std::make_move_iterator(src.end())); + + // Vojta wants back compatibility + src.clear(); + src.shrink_to_fit(); } - src.clear(); - src.shrink_to_fit(); } template // Arbitrary allocator can be used @@ -140,8 +145,8 @@ void clear_and_shrink(std::vector& vec) template inline void append_reversed(std::vector& dest, const std::vector& src) { - if (dest.empty()) - dest = src; + if (dest.empty()) + dest = {src.rbegin(), src.rend()}; else dest.insert(dest.end(), src.rbegin(), src.rend()); } @@ -151,11 +156,14 @@ template inline void append_reversed(std::vector& dest, std::vector&& src) { if (dest.empty()) - dest = std::move(src); - else { - dest.reserve(dest.size() + src.size()); - std::move(std::rbegin(src), std::rend(src), std::back_inserter(dest)); - } + dest = {std::make_move_iterator(src.rbegin), + std::make_move_iterator(src.rend)}; + else + dest.insert(dest.end(), + std::make_move_iterator(src.rbegin()), + std::make_move_iterator(src.rend())); + + // Vojta wants back compatibility src.clear(); src.shrink_to_fit(); } @@ -268,6 +276,14 @@ constexpr inline bool is_approx(Number value, Number test_value, Number precisio return std::fabs(double(value) - double(test_value)) < double(precision); } +template +constexpr inline bool is_approx(const std::optional &value, + const std::optional &test_value) +{ + return (!value.has_value() && !test_value.has_value()) || + (value.has_value() && test_value.has_value() && is_approx(*value, *test_value)); +} + // A meta-predicate which is true for integers wider than or equal to coord_t template struct is_scaled_coord { diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index d9ca1d5326..2cca334d9d 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -944,7 +944,34 @@ std::string xml_escape(std::string text, bool is_marked/* = false*/) case '\'': replacement = "'"; break; case '&': replacement = "&"; break; case '<': replacement = is_marked ? "<" :"<"; break; - case '>': replacement = is_marked ? ">" :">"; break; + case '>': replacement = is_marked ? ">" : ">"; break; + default: break; + } + + text.replace(pos, 1, replacement); + pos += replacement.size(); + } + + return text; +} + +// Definition of escape symbols https://www.w3.org/TR/REC-xml/#AVNormalize + +std::string xml_escape_double_quotes_attribute_value(std::string text) +{ + std::string::size_type pos = 0; + for (;;) { + pos = text.find_first_of("\"&<\r\n\t", pos); + if (pos == std::string::npos) break; + + std::string replacement; + switch (text[pos]) { + case '\"': replacement = """; break; + case '&': replacement = "&"; break; + case '<': replacement = "<"; break; + case '\r': replacement = " "; break; + case '\n': replacement = " "; break; + case '\t': replacement = " "; break; default: break; } diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index d37221b7b3..4fa51c4868 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -39,6 +39,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmosCommon.hpp GUI/Gizmos/GLGizmoBase.cpp GUI/Gizmos/GLGizmoBase.hpp + GUI/Gizmos/GLGizmoEmboss.cpp + GUI/Gizmos/GLGizmoEmboss.hpp GUI/Gizmos/GLGizmoMove.cpp GUI/Gizmos/GLGizmoMove.hpp GUI/Gizmos/GLGizmoRotate.cpp @@ -139,6 +141,8 @@ set(SLIC3R_GUI_SOURCES GUI/CoordAxes.hpp GUI/Camera.cpp GUI/Camera.hpp + GUI/CameraUtils.cpp + GUI/CameraUtils.hpp GUI/wxExtensions.cpp GUI/wxExtensions.hpp GUI/ExtruderSequenceDialog.cpp @@ -185,6 +189,12 @@ set(SLIC3R_GUI_SOURCES GUI/Jobs/PlaterWorker.hpp GUI/Jobs/ArrangeJob.hpp GUI/Jobs/ArrangeJob.cpp + GUI/Jobs/CreateFontNameImageJob.cpp + GUI/Jobs/CreateFontNameImageJob.hpp + GUI/Jobs/CreateFontStyleImagesJob.cpp + GUI/Jobs/CreateFontStyleImagesJob.hpp + GUI/Jobs/EmbossJob.cpp + GUI/Jobs/EmbossJob.hpp GUI/Jobs/RotoptimizeJob.hpp GUI/Jobs/RotoptimizeJob.cpp GUI/Jobs/FillBedJob.hpp @@ -232,8 +242,14 @@ set(SLIC3R_GUI_SOURCES Utils/OctoPrint.hpp Utils/Duet.cpp Utils/Duet.hpp + Utils/EmbossStyleManager.cpp + Utils/EmbossStyleManager.hpp + Utils/EmbossStylesSerializable.cpp + Utils/EmbossStylesSerializable.hpp Utils/FlashAir.cpp Utils/FlashAir.hpp + Utils/FontConfigHelp.cpp + Utils/FontConfigHelp.hpp Utils/AstroBox.cpp Utils/AstroBox.hpp Utils/Repetier.cpp @@ -246,6 +262,8 @@ set(SLIC3R_GUI_SOURCES Utils/PresetUpdater.hpp Utils/Process.cpp Utils/Process.hpp + Utils/RaycastManager.cpp + Utils/RaycastManager.hpp Utils/UndoRedo.cpp Utils/UndoRedo.hpp Utils/HexFile.cpp @@ -256,6 +274,8 @@ set(SLIC3R_GUI_SOURCES Utils/MKS.hpp Utils/WinRegistry.cpp Utils/WinRegistry.hpp + Utils/WxFontUtils.cpp + Utils/WxFontUtils.hpp ) find_package(NanoSVG REQUIRED) @@ -310,5 +330,5 @@ endif () if (UNIX AND NOT APPLE) find_package(GTK${SLIC3R_GTK} REQUIRED) target_include_directories(libslic3r_gui PRIVATE ${GTK${SLIC3R_GTK}_INCLUDE_DIRS}) - target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES}) + target_link_libraries(libslic3r_gui ${GTK${SLIC3R_GTK}_LIBRARIES} fontconfig) endif () diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index a08694a3d8..e65a321f1d 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -324,64 +324,73 @@ void GLVolume::SinkingContours::update() const int object_idx = m_parent.object_idx(); const Model& model = GUI::wxGetApp().plater()->model(); - if (0 <= object_idx && object_idx < int(model.objects.size()) && m_parent.is_sinking() && !m_parent.is_below_printbed()) { - const BoundingBoxf3& box = m_parent.transformed_convex_hull_bounding_box(); - if (!m_old_box.size().isApprox(box.size()) || m_old_box.min.z() != box.min.z()) { - m_old_box = box; - m_shift = Vec3d::Zero(); - - const TriangleMesh& mesh = model.objects[object_idx]->volumes[m_parent.volume_idx()]->mesh(); - - m_model.reset(); - GUI::GLModel::Geometry init_data; -#if ENABLE_LEGACY_OPENGL_REMOVAL - init_data.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3 }; - init_data.color = ColorRGBA::WHITE(); - unsigned int vertices_counter = 0; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - MeshSlicingParams slicing_params; - slicing_params.trafo = m_parent.world_matrix(); - const Polygons polygons = union_(slice_mesh(mesh.its, 0.0f, slicing_params)); - for (const ExPolygon& expoly : diff_ex(expand(polygons, float(scale_(HalfWidth))), shrink(polygons, float(scale_(HalfWidth))))) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - const std::vector triangulation = triangulate_expolygon_3d(expoly); - init_data.reserve_vertices(init_data.vertices_count() + triangulation.size()); - init_data.reserve_indices(init_data.indices_count() + triangulation.size()); - for (const Vec3d& v : triangulation) { - init_data.add_vertex((Vec3f)(v.cast() + 0.015f * Vec3f::UnitZ())); // add a small positive z to avoid z-fighting - ++vertices_counter; - if (vertices_counter % 3 == 0) - init_data.add_triangle(vertices_counter - 3, vertices_counter - 2, vertices_counter - 1); - } - } - m_model.init_from(std::move(init_data)); -#else - GUI::GLModel::Geometry::Entity entity; - entity.type = GUI::GLModel::EPrimitiveType::Triangles; - const std::vector triangulation = triangulate_expolygon_3d(expoly); - entity.positions.reserve(entity.positions.size() + triangulation.size()); - entity.normals.reserve(entity.normals.size() + triangulation.size()); - entity.indices.reserve(entity.indices.size() + triangulation.size() / 3); - for (const Vec3d& v : triangulation) { - entity.positions.emplace_back(v.cast() + 0.015f * Vec3f::UnitZ()); // add a small positive z to avoid z-fighting - entity.normals.emplace_back(Vec3f::UnitZ()); - const size_t positions_count = entity.positions.size(); - if (positions_count % 3 == 0) { - entity.indices.emplace_back(positions_count - 3); - entity.indices.emplace_back(positions_count - 2); - entity.indices.emplace_back(positions_count - 1); - } - } - init_data.entities.emplace_back(entity); - } - m_model.init_from(init_data); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - else - m_shift = box.center() - m_old_box.center(); - } - else + if (object_idx < 0 || + object_idx >= int(model.objects.size()) || + !m_parent.is_sinking() || + m_parent.is_below_printbed()){ m_model.reset(); + return; + } + + const BoundingBoxf3& box = m_parent.transformed_convex_hull_bounding_box(); + if (m_old_box.size().isApprox(box.size()) && + m_old_box.min.z() == box.min.z()){ + // Fix it !!! It is not working all the time + m_shift = box.center() - m_old_box.center(); + return; + } + + m_old_box = box; + m_shift = Vec3d::Zero(); + + const TriangleMesh& mesh = model.objects[object_idx]->volumes[m_parent.volume_idx()]->mesh(); + + m_model.reset(); + GUI::GLModel::Geometry init_data; +#if ENABLE_LEGACY_OPENGL_REMOVAL + init_data.format = { GUI::GLModel::Geometry::EPrimitiveType::Triangles, GUI::GLModel::Geometry::EVertexLayout::P3 }; + init_data.color = ColorRGBA::WHITE(); + unsigned int vertices_counter = 0; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + MeshSlicingParams slicing_params; + slicing_params.trafo = m_parent.world_matrix(); + const Polygons polygons = union_(slice_mesh(mesh.its, 0.0f, slicing_params)); + if (polygons.empty()) return; + + for (const ExPolygon& expoly : diff_ex(expand(polygons, float(scale_(HalfWidth))), shrink(polygons, float(scale_(HalfWidth))))) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + const std::vector triangulation = triangulate_expolygon_3d(expoly); + init_data.reserve_vertices(init_data.vertices_count() + triangulation.size()); + init_data.reserve_indices(init_data.indices_count() + triangulation.size()); + for (const Vec3d& v : triangulation) { + init_data.add_vertex((Vec3f)(v.cast() + 0.015f * Vec3f::UnitZ())); // add a small positive z to avoid z-fighting + ++vertices_counter; + if (vertices_counter % 3 == 0) + init_data.add_triangle(vertices_counter - 3, vertices_counter - 2, vertices_counter - 1); + } + } + m_model.init_from(std::move(init_data)); +#else + GUI::GLModel::Geometry::Entity entity; + entity.type = GUI::GLModel::EPrimitiveType::Triangles; + const std::vector triangulation = triangulate_expolygon_3d(expoly); + entity.positions.reserve(entity.positions.size() + triangulation.size()); + entity.normals.reserve(entity.normals.size() + triangulation.size()); + entity.indices.reserve(entity.indices.size() + triangulation.size() / 3); + for (const Vec3d& v : triangulation) { + entity.positions.emplace_back(v.cast() + 0.015f * Vec3f::UnitZ()); // add a small positive z to avoid z-fighting + entity.normals.emplace_back(Vec3f::UnitZ()); + const size_t positions_count = entity.positions.size(); + if (positions_count % 3 == 0) { + entity.indices.emplace_back(positions_count - 3); + entity.indices.emplace_back(positions_count - 2); + entity.indices.emplace_back(positions_count - 1); + } + } + init_data.entities.emplace_back(entity); + } + m_model.init_from(init_data); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL } void GLVolume::NonManifoldEdges::render() @@ -1291,9 +1300,15 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disab #endif // ENABLE_GL_CORE_PROFILE #endif // ENABLE_LEGACY_OPENGL_REMOVAL + ScopeGuard transparent_sg; if (type == ERenderType::Transparent) { glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + glsafe(::glDepthMask(false)); + transparent_sg = ScopeGuard([]() { + glsafe(::glDisable(GL_BLEND)); + glsafe(::glDepthMask(true)); + }); } glsafe(::glCullFace(GL_BACK)); @@ -1421,9 +1436,6 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, bool disab if (disable_cullface) glsafe(::glEnable(GL_CULL_FACE)); - - if (type == ERenderType::Transparent) - glsafe(::glDisable(GL_BLEND)); } bool GLVolumeCollection::check_outside_state(const BuildVolume &build_volume, ModelInstanceEPrintVolumeState *out_state) const diff --git a/src/slic3r/GUI/CameraUtils.cpp b/src/slic3r/GUI/CameraUtils.cpp new file mode 100644 index 0000000000..c99616b34e --- /dev/null +++ b/src/slic3r/GUI/CameraUtils.cpp @@ -0,0 +1,123 @@ +#include "CameraUtils.hpp" +#include // projecting points + +#include "slic3r/GUI/3DScene.hpp" // GLVolume +#include "libslic3r/Geometry/ConvexHull.hpp" + +using namespace Slic3r; +using namespace GUI; + +Points CameraUtils::project(const Camera & camera, + const std::vector &points) +{ + Vec4i viewport(camera.get_viewport().data()); + + // Convert our std::vector to Eigen dynamic matrix. + Eigen::Matrix + pts(points.size(), 3); + for (size_t i = 0; i < points.size(); ++i) + pts.block<1, 3>(i, 0) = points[i]; + + // Get the projections. + Eigen::Matrix projections; + igl::project(pts, camera.get_view_matrix().matrix(), + camera.get_projection_matrix().matrix(), viewport, projections); + + Points result; + result.reserve(points.size()); + int window_height = viewport[3]; + + // convert to points --> loss precision + for (int i = 0; i < projections.rows(); ++i) { + double x = projections(i, 0); + double y = projections(i, 1); + // opposit direction o Y + result.emplace_back(x, window_height - y); + } + return result; +} + +Point CameraUtils::project(const Camera &camera, const Vec3d &point) +{ + // IMPROVE: do it faster when you need it (inspire in project multi point) + return project(camera, std::vector{point}).front(); +} + +Slic3r::Polygon CameraUtils::create_hull2d(const Camera & camera, + const GLVolume &volume) +{ + std::vector vertices; + const TriangleMesh *hull = volume.convex_hull(); + if (hull != nullptr) { + const indexed_triangle_set &its = hull->its; + vertices.reserve(its.vertices.size()); + // cast vector + for (const Vec3f &vertex : its.vertices) + vertices.emplace_back(vertex.cast()); + } else { + // Negative volume doesn't have convex hull so use bounding box + auto bb = volume.bounding_box(); + Vec3d &min = bb.min; + Vec3d &max = bb.max; + vertices = {min, + Vec3d(min.x(), min.y(), max.z()), + Vec3d(min.x(), max.y(), min.z()), + Vec3d(min.x(), max.y(), max.z()), + Vec3d(max.x(), min.y(), min.z()), + Vec3d(max.x(), min.y(), max.z()), + Vec3d(max.x(), max.y(), min.z()), + max}; + } + + const Transform3d &trafoMat = + volume.get_instance_transformation().get_matrix() * + volume.get_volume_transformation().get_matrix(); + for (Vec3d &vertex : vertices) + vertex = trafoMat * vertex.cast(); + + Points vertices_2d = project(camera, vertices); + return Geometry::convex_hull(vertices_2d); +} + +#include +Vec3d CameraUtils::create_ray(const Camera &camera, const Vec2d &coor) { + if (camera.get_type() == Camera::EType::Ortho) + return camera.get_dir_forward(); + // check that it is known camera no other tha ORTHO or Persepective + assert(camera.get_type() == Camera::EType::Perspective); + + Matrix4d modelview = camera.get_view_matrix().matrix(); + Matrix4d projection = camera.get_projection_matrix().matrix(); + Vec4i viewport(camera.get_viewport().data()); + + Vec3d scene_point(coor.x(), viewport[3] - coor.y(), 0.); + Vec3d unprojected_point; + igl::unproject(scene_point, modelview, projection, viewport, unprojected_point); + + Vec3d p0 = camera.get_position(); + Vec3d dir = unprojected_point - p0; + dir.normalize(); + return dir; +} + +Vec2d CameraUtils::get_z0_position(const Camera &camera, const Vec2d & coor) +{ + Vec3d dir = CameraUtils::create_ray(camera, coor); + Vec3d p0 = camera.get_position(); + if (camera.get_type() == Camera::EType::Ortho) { + Matrix4d modelview = camera.get_view_matrix().matrix(); + Matrix4d projection = camera.get_projection_matrix().matrix(); + Vec4i viewport(camera.get_viewport().data()); + igl::unproject(Vec3d(coor.x(), viewport[3] - coor.y(), 0.), modelview, projection, viewport, p0); + } + + // is approx zero + if ((fabs(dir.z()) - 1e-4) < 0) + return Vec2d(std::numeric_limits::max(), + std::numeric_limits::max()); + + // find position of ray cross plane(z = 0) + double t = p0.z() / dir.z(); + Vec3d p = p0 - t * dir; + return Vec2d(p.x(), p.y()); +} diff --git a/src/slic3r/GUI/CameraUtils.hpp b/src/slic3r/GUI/CameraUtils.hpp new file mode 100644 index 0000000000..7d1b43d64e --- /dev/null +++ b/src/slic3r/GUI/CameraUtils.hpp @@ -0,0 +1,58 @@ +#ifndef slic3r_CameraUtils_hpp_ +#define slic3r_CameraUtils_hpp_ + +#include "Camera.hpp" +#include "libslic3r/Point.hpp" +namespace Slic3r { +class GLVolume; +} + +namespace Slic3r::GUI { +/// +/// Help divide camera data and camera functions +/// This utility work with camera data by static funtions +/// +class CameraUtils +{ +public: + CameraUtils() = delete; // only static functions + + /// + /// Project point throw camera to 2d coordinate into imgui window + /// + /// Projection params + /// Point to project. + /// projected points by camera into coordinate of camera. + /// x(from left to right), y(from top to bottom) + static Points project(const Camera& camera, const std::vector &points); + static Point project(const Camera& camera, const Vec3d &point); + + /// + /// Create hull around GLVolume in 2d space of camera + /// + /// Projection params + /// Outline by 3d object + /// Polygon around object + static Polygon create_hull2d(const Camera &camera, const GLVolume &volume); + + /// + /// Unproject screen coordinate to scene direction start from camera position + /// + /// Projection params + /// Coordinate on screen + /// Scene direction + static Vec3d create_ray(const Camera &camera, const Vec2d &coor); + + /// + /// Unproject mouse coordinate to get position in space where z coor is zero + /// Platter surface should be in z == 0 + /// + /// Projection params + /// Mouse position + /// Position on platter under mouse + static Vec2d get_z0_position(const Camera &camera, const Vec2d &coor); + +}; +} // Slic3r::GUI + +#endif /* slic3r_CameraUtils_hpp_ */ diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index aa3130b71e..11d1bb80c3 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3727,6 +3727,24 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) else evt.Skip(); + // Detection of doubleclick on text to open emboss edit window + if (evt.LeftDClick() && m_gizmos.get_current() == nullptr && !m_hover_volume_idxs.empty()) { + for (int hover_volume_id : m_hover_volume_idxs) { + const GLVolume &hover_gl_volume = *m_volumes.volumes[hover_volume_id]; + const ModelObject* hover_object = m_model->objects[hover_gl_volume.object_idx()]; + int hover_volume_idx = hover_gl_volume.volume_idx(); + const ModelVolume* hover_volume = hover_object->volumes[hover_volume_idx]; + if (hover_volume->text_configuration.has_value()) { + //m_selection.set_mode(Selection::EMode::Volume); + //m_selection.add(hover_volume_id); // add whole instance + m_selection.add_volumes(Selection::EMode::Volume, {(unsigned) hover_volume_id}); + m_gizmos.open_gizmo(GLGizmosManager::EType::Emboss); + wxGetApp().obj_list()->update_selections(); + return; + } + } + } + if (m_moving) show_sinking_contours(); @@ -4382,6 +4400,14 @@ bool GLCanvas3D::is_object_sinking(int object_idx) const return false; } +void GLCanvas3D::apply_retina_scale(Vec2d &screen_coordinate) const +{ +#if ENABLE_RETINA_GL + double scale = static_cast(m_retina_helper->get_scale_factor()); + screen_coordinate *= scale; +#endif // ENABLE_RETINA_GL +} + bool GLCanvas3D::_is_shown_on_screen() const { return (m_canvas != nullptr) ? m_canvas->IsShownOnScreen() : false; @@ -7246,7 +7272,7 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c _3DScene::extrusionentity_to_verts(layerm->perimeters(), float(layer->print_z), copy, select_geometry(idx_layer, layerm->region().config().perimeter_extruder.value, 0)); #else - _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy, + _3DScene::extrusionentity_to_verts(layerm->perimeters(), float(layer->print_z), copy, volume(idx_layer, layerm->region().config().perimeter_extruder.value, 0)); #endif // ENABLE_LEGACY_OPENGL_REMOVAL if (ctxt.has_infill) { diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index a5ec9c1914..a519a26a19 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -830,6 +830,12 @@ public: Size get_canvas_size() const; Vec2d get_local_mouse_position() const; + // store opening position of menu + std::optional m_popup_menu_positon; // position of mouse right click + void set_popup_menu_position(const Vec2d &position) { m_popup_menu_positon = position; } + const std::optional& get_popup_menu_position() const { return m_popup_menu_positon; } + void clear_popup_menu_position() { m_popup_menu_positon.reset(); } + void set_tooltip(const std::string& tooltip); // the following methods add a snapshot to the undo/redo stack, unless the given string is empty @@ -961,6 +967,8 @@ public: bool is_object_sinking(int object_idx) const; + void apply_retina_scale(Vec2d &screen_coordinate) const; + private: bool _is_shown_on_screen() const; diff --git a/src/slic3r/GUI/GLSelectionRectangle.cpp b/src/slic3r/GUI/GLSelectionRectangle.cpp index 5959c19510..a2c93184b8 100644 --- a/src/slic3r/GUI/GLSelectionRectangle.cpp +++ b/src/slic3r/GUI/GLSelectionRectangle.cpp @@ -1,5 +1,6 @@ #include "GLSelectionRectangle.hpp" #include "Camera.hpp" +#include "CameraUtils.hpp" #include "3DScene.hpp" #include "GLCanvas3D.hpp" #include "GUI_App.hpp" @@ -44,30 +45,15 @@ namespace GUI { m_state = EState::Off; #endif // !ENABLE_RAYCAST_PICKING - const Camera& camera = wxGetApp().plater()->get_camera(); - const Matrix4d modelview = camera.get_view_matrix().matrix(); - const Matrix4d projection= camera.get_projection_matrix().matrix(); - const Vec4i viewport(camera.get_viewport().data()); - - // Convert our std::vector to Eigen dynamic matrix. - Eigen::Matrix pts(points.size(), 3); - for (size_t i=0; i(i, 0) = points[i]; - - // Get the projections. - Eigen::Matrix projections; - igl::project(pts, modelview, projection, viewport, projections); - // bounding box created from the rectangle corners - will take care of order of the corners const BoundingBox rectangle(Points{ Point(m_start_corner.cast()), Point(m_end_corner.cast()) }); // Iterate over all points and determine whether they're in the rectangle. - for (int i = 0; iget_camera(); + Points points_2d = CameraUtils::project(camera, points); + unsigned int size = static_cast(points.size()); + for (unsigned int i = 0; i< size; ++i) + if (rectangle.contains(points_2d[i])) out.push_back(i); return out; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 7042d599b9..161784d959 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -469,6 +469,7 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = { /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv, ".step"sv, ".stp"sv } }, /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, + /* FT_FONTS */ { "Font files"sv, { ".ttc"sv, ".ttf"sv } }, /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, /* FT_INI */ { "INI files"sv, { ".ini"sv } }, diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index a329995e0c..682aa1d001 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -62,6 +62,7 @@ enum FileType FT_GCODE, FT_MODEL, FT_PROJECT, + FT_FONTS, FT_GALLERY, FT_INI, diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 6ffe19218d..0ae1b8870a 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -13,6 +13,7 @@ #include "GLCanvas3D.hpp" #include "Selection.hpp" #include "format.hpp" +#include "Gizmos/GLGizmoEmboss.hpp" #include #include "slic3r/Utils/FixModelByWin10.hpp" @@ -163,6 +164,14 @@ const std::vector> MenuFactory::ADD_VOLUME_M {L("Add support enforcer"), "support_enforcer"}, // ~ModelVolumeType::SUPPORT_ENFORCER }; +// Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important +const std::vector> MenuFactory::TEXT_VOLUME_ICONS { +// menu_item Name menu_item bitmap name + {L("Add text"), "add_text_part"}, // ~ModelVolumeType::MODEL_PART + {L("Add negative text"), "add_text_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME + {L("Add text modifier"), "add_text_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER +}; + static Plater* plater() { return wxGetApp().plater(); @@ -438,6 +447,15 @@ std::vector MenuFactory::get_volume_bitmaps() return volume_bmps; } +std::vector MenuFactory::get_text_volume_bitmaps() +{ + std::vector volume_bmps; + volume_bmps.reserve(TEXT_VOLUME_ICONS.size()); + for (auto item : TEXT_VOLUME_ICONS) + volume_bmps.push_back(get_bmp_bundle(item.second)); + return volume_bmps; +} + void MenuFactory::append_menu_item_delete(wxMenu* menu) { append_menu_item(menu, wxID_ANY, _L("Delete") + "\tDel", _L("Remove the selected object"), @@ -451,21 +469,27 @@ void MenuFactory::append_menu_item_delete(wxMenu* menu) wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType type) { auto sub_menu = new wxMenu; - if (wxGetApp().get_mode() == comExpert && type != ModelVolumeType::INVALID) { + const ConfigOptionMode mode = wxGetApp().get_mode(); + + if (mode == comExpert && type != ModelVolumeType::INVALID || + mode == comAdvanced && type == ModelVolumeType::MODEL_PART) { append_menu_item(sub_menu, wxID_ANY, _L("Load") + " " + dots, "", [type](wxCommandEvent&) { obj_list()->load_subobject(type); }, "", menu); sub_menu->AppendSeparator(); } - for (auto& item : { L("Box"), L("Cylinder"), L("Sphere"), L("Slab") }) - { - if (type == ModelVolumeType::INVALID && strncmp(item, "Slab", 4) == 0) - continue; - append_menu_item(sub_menu, wxID_ANY, _(item), "", - [type, item](wxCommandEvent&) { obj_list()->load_generic_subobject(item, type); }, "", menu); - } + if (!(type == ModelVolumeType::MODEL_PART && mode == comAdvanced)) + for (auto& item : { L("Box"), L("Cylinder"), L("Sphere"), L("Slab") }) + { + if (type == ModelVolumeType::INVALID && strncmp(item, "Slab", 4) == 0) + continue; + append_menu_item(sub_menu, wxID_ANY, _(item), "", + [type, item](wxCommandEvent&) { obj_list()->load_generic_subobject(item, type); }, "", menu); + } - if (wxGetApp().get_mode() >= comAdvanced) { + append_menu_item_add_text(sub_menu, type); + + if (mode >= comAdvanced) { sub_menu->AppendSeparator(); append_menu_item(sub_menu, wxID_ANY, _L("Gallery"), "", [type](wxCommandEvent&) { obj_list()->load_subobject(type, true); }, "", menu); @@ -474,30 +498,66 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty return sub_menu; } +void MenuFactory::append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item/* = true*/) +{ + auto add_text = [type](wxCommandEvent& evt) { + GLCanvas3D* canvas = plater()->canvas3D(); + GLGizmosManager& mng = canvas->get_gizmos_manager(); + GLGizmoBase* gizmo = mng.get_gizmo(GLGizmosManager::Emboss); + GLGizmoEmboss* emboss = dynamic_cast(gizmo); + assert(emboss != nullptr); + if (emboss == nullptr) return; + + ModelVolumeType volume_type = type; + // no selected object means create new object + if (volume_type == ModelVolumeType::INVALID) + volume_type = ModelVolumeType::MODEL_PART; + + auto screen_position = canvas->get_popup_menu_position(); + if (screen_position.has_value()) { + emboss->create_volume(volume_type, *screen_position); + } else { + emboss->create_volume(volume_type); + } + }; + + if ( type == ModelVolumeType::MODEL_PART + || type == ModelVolumeType::NEGATIVE_VOLUME + || type == ModelVolumeType::PARAMETER_MODIFIER + || type == ModelVolumeType::INVALID // cannot use gizmo without selected object + ) { + wxString item_name = is_submenu_item ? "" : _(ADD_VOLUME_MENU_ITEMS[int(type)].first) + ": "; + item_name += _L("Text"); + const std::string icon_name = is_submenu_item ? "" : ADD_VOLUME_MENU_ITEMS[int(type)].second; + append_menu_item(menu, wxID_ANY, item_name, "", add_text, icon_name, menu); + } +} + void MenuFactory::append_menu_items_add_volume(wxMenu* menu) { - // Update "add" items(delete old & create new) settings popupmenu + // Update "add" items(delete old & create new) items popupmenu for (auto& item : ADD_VOLUME_MENU_ITEMS) { - const auto settings_id = menu->FindItem(_(item.first)); - if (settings_id != wxNOT_FOUND) - menu->Destroy(settings_id); + const wxString item_name = _(item.first); + int item_id = menu->FindItem(item_name); + if (item_id != wxNOT_FOUND) + menu->Destroy(item_id); + + item_id = menu->FindItem(item_name + ": " + _L("Text")); + if (item_id != wxNOT_FOUND) + menu->Destroy(item_id); } // Update "Height range Modifier" item (delete old & create new) if (const auto range_id = menu->FindItem(_L("Height range Modifier")); range_id != wxNOT_FOUND) menu->Destroy(range_id); - const ConfigOptionMode mode = wxGetApp().get_mode(); + if (const auto range_id = menu->FindItem(_L("Height range Modifier")); range_id != wxNOT_FOUND) + menu->Destroy(range_id); + + if (wxGetApp().get_mode() == comSimple) { + append_menu_item_add_text(menu, ModelVolumeType::MODEL_PART, false); + append_menu_item_add_text(menu, ModelVolumeType::NEGATIVE_VOLUME, false); - if (mode == comAdvanced) { - append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::MODEL_PART)].first), "", - [](wxCommandEvent&) { obj_list()->load_subobject(ModelVolumeType::MODEL_PART); }, - ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::MODEL_PART)].second, nullptr, - []() { return obj_list()->is_instance_or_object_selected() - && !obj_list()->is_selected_object_cut(); - }, m_parent); - } - if (mode == comSimple) { append_menu_item(menu, wxID_ANY, _(ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_ENFORCER)].first), "", [](wxCommandEvent&) { obj_list()->load_generic_subobject(L("Box"), ModelVolumeType::SUPPORT_ENFORCER); }, ADD_VOLUME_MENU_ITEMS[int(ModelVolumeType::SUPPORT_ENFORCER)].second, nullptr, @@ -510,11 +570,9 @@ void MenuFactory::append_menu_items_add_volume(wxMenu* menu) return; } - for (size_t type = (mode == comExpert ? 0 : 1); type < ADD_VOLUME_MENU_ITEMS.size(); type++) - { - auto& item = ADD_VOLUME_MENU_ITEMS[type]; - - wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type)); + int type = 0; + for (auto& item : ADD_VOLUME_MENU_ITEMS) { + wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type++)); append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", item.second, [type]() { bool can_add = type < size_t(ModelVolumeType::PARAMETER_MODIFIER) ? !obj_list()->is_selected_object_cut() : true; @@ -907,6 +965,37 @@ void MenuFactory::append_menu_items_mirror(wxMenu* menu) []() { return plater()->can_mirror(); }, m_parent); } +void MenuFactory::append_menu_item_edit_text(wxMenu *menu) +{ + wxString name = _L("Edit text"); + + auto can_edit_text = []() { + const auto& sel = plater()->get_selection(); + if (sel.volumes_count() != 1) return false; + auto cid = sel.get_volume(*sel.get_volume_idxs().begin()); + const ModelVolume* vol = plater()->canvas3D()->get_model() + ->objects[cid->object_idx()]->volumes[cid->volume_idx()]; + return vol->text_configuration.has_value(); + }; + + if (menu == &m_object_menu) { + auto menu_item_id = menu->FindItem(name); + if (menu_item_id != wxNOT_FOUND) + menu->Destroy(menu_item_id); + if (!can_edit_text()) + return; + } + + wxString description = _L("Ability to change text, font, size, ..."); + std::string icon = ""; + append_menu_item( + menu, wxID_ANY, name, description, + [can_edit_text](wxCommandEvent &) { + plater()->canvas3D()->get_gizmos_manager().open_gizmo(GLGizmosManager::Emboss); + }, + icon, nullptr, can_edit_text, m_parent); +} + MenuFactory::MenuFactory() { for (int i = 0; i < mtCount; i++) { @@ -979,6 +1068,20 @@ void MenuFactory::create_sla_object_menu() m_sla_object_menu.AppendSeparator(); } +void MenuFactory::append_immutable_part_menu_items(wxMenu* menu) +{ + append_menu_items_mirror(menu); + + menu->AppendSeparator(); + append_menu_item_change_type(menu); +} + +void MenuFactory::append_mutable_part_menu_items(wxMenu* menu) +{ + append_menu_item_settings(menu); + append_menu_item_change_extruder(menu); +} + void MenuFactory::create_part_menu() { wxMenu* menu = &m_part_menu; @@ -991,15 +1094,24 @@ void MenuFactory::create_part_menu() append_menu_item_export_stl(menu); append_menu_item_fix_through_netfabb(menu); append_menu_item_simplify(menu); - append_menu_items_mirror(menu); append_menu_item(menu, wxID_ANY, _L("Split"), _L("Split the selected object into individual parts"), - [](wxCommandEvent&) { plater()->split_volume(); }, "split_parts_SMALL", nullptr, + [](wxCommandEvent&) { plater()->split_volume(); }, "split_parts_SMALL", nullptr, []() { return plater()->can_split(false); }, m_parent); - menu->AppendSeparator(); - append_menu_item_change_type(menu); + append_immutable_part_menu_items(menu); +} +void MenuFactory::create_text_part_menu() +{ + wxMenu* menu = &m_text_part_menu; + + append_menu_item_delete(menu); + append_menu_item_edit_text(menu); + append_menu_item_fix_through_netfabb(menu); + append_menu_item_simplify(menu); + + append_immutable_part_menu_items(menu); } void MenuFactory::create_instance_menu() @@ -1018,6 +1130,7 @@ void MenuFactory::init(wxWindow* parent) create_object_menu(); create_sla_object_menu(); create_part_menu(); + create_text_part_menu(); create_instance_menu(); } @@ -1039,6 +1152,7 @@ wxMenu* MenuFactory::object_menu() append_menu_item_change_extruder(&m_object_menu); update_menu_items_instance_manipulation(mtObjectFFF); append_menu_item_invalidate_cut_info(&m_object_menu); + append_menu_item_edit_text(&m_object_menu); return &m_object_menu; } @@ -1049,6 +1163,7 @@ wxMenu* MenuFactory::sla_object_menu() append_menu_item_settings(&m_sla_object_menu); update_menu_items_instance_manipulation(mtObjectSLA); append_menu_item_invalidate_cut_info(&m_sla_object_menu); + append_menu_item_edit_text(&m_sla_object_menu); return &m_sla_object_menu; } @@ -1056,12 +1171,19 @@ wxMenu* MenuFactory::sla_object_menu() wxMenu* MenuFactory::part_menu() { append_menu_items_convert_unit(&m_part_menu, 2); - append_menu_item_settings(&m_part_menu); - append_menu_item_change_extruder(&m_part_menu); + + append_mutable_part_menu_items(&m_part_menu); return &m_part_menu; } +wxMenu* MenuFactory::text_part_menu() +{ + append_mutable_part_menu_items(&m_text_part_menu); + + return &m_text_part_menu; +} + wxMenu* MenuFactory::instance_menu() { return &m_instance_menu; diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index 7190edb64c..ed27655be0 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -34,7 +34,9 @@ class MenuFactory { public: static const std::vector> ADD_VOLUME_MENU_ITEMS; - static std::vector get_volume_bitmaps(); + static const std::vector> TEXT_VOLUME_ICONS; + static std::vector get_volume_bitmaps(); + static std::vector get_text_volume_bitmaps(); MenuFactory(); ~MenuFactory() = default; @@ -51,6 +53,7 @@ public: wxMenu* object_menu(); wxMenu* sla_object_menu(); wxMenu* part_menu(); + wxMenu* text_part_menu(); wxMenu* instance_menu(); wxMenu* layer_menu(); wxMenu* multi_selection_menu(); @@ -66,6 +69,7 @@ private: MenuWithSeparators m_object_menu; MenuWithSeparators m_part_menu; + MenuWithSeparators m_text_part_menu; MenuWithSeparators m_sla_object_menu; MenuWithSeparators m_default_menu; MenuWithSeparators m_instance_menu; @@ -79,10 +83,14 @@ private: void create_common_object_menu(wxMenu *menu); void create_object_menu(); void create_sla_object_menu(); + void append_immutable_part_menu_items(wxMenu* menu); + void append_mutable_part_menu_items(wxMenu* menu); void create_part_menu(); + void create_text_part_menu(); void create_instance_menu(); wxMenu* append_submenu_add_generic(wxMenu* menu, ModelVolumeType type); + void append_menu_item_add_text(wxMenu* menu, ModelVolumeType type, bool is_submenu_item = true); void append_menu_items_add_volume(wxMenu* menu); wxMenuItem* append_menu_item_layers_editing(wxMenu* menu); wxMenuItem* append_menu_item_settings(wxMenu* menu); @@ -103,6 +111,7 @@ private: void append_menu_item_merge_to_multipart_object(wxMenu *menu); // void append_menu_item_merge_to_single_object(wxMenu *menu); void append_menu_items_mirror(wxMenu *menu); + void append_menu_item_edit_text(wxMenu *menu); void append_menu_items_instance_manipulation(wxMenu *menu); void update_menu_items_instance_manipulation(MenuType type); }; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index fdce59f677..127d97caa2 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1,5 +1,6 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/TextConfiguration.hpp" #include "GUI_ObjectList.hpp" #include "GUI_Factories.hpp" #include "GUI_ObjectManipulation.hpp" @@ -649,14 +650,22 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const ModelObject* obj = object(obj_idx); if (m_objects_model->GetItemType(item) & itObject) { - obj->name = m_objects_model->GetName(item).ToUTF8().data(); + obj->name = into_u8(m_objects_model->GetName(item)); // if object has just one volume, rename this volume too - if (obj->volumes.size() == 1) + if (obj->volumes.size() == 1 && !obj->volumes[0]->text_configuration.has_value()) obj->volumes[0]->name = obj->name; return; } - if (volume_id < 0) return; + if (volume_id < 0) + return; + + // Renaming of the text volume is suppressed + // So, revert the name in object list + if (obj->volumes[volume_id]->text_configuration.has_value()) { + m_objects_model->SetName(from_u8(obj->volumes[volume_id]->name), item); + return; + } obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data(); } @@ -672,6 +681,10 @@ void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const return; m_objects_model->SetName(new_name, item); + + // if object has just one volume, rename object too + if (ModelObject* obj = object(obj_idx); obj->volumes.size() == 1) + obj->name = obj->volumes.front()->name; } void ObjectList::selection_changed() @@ -968,11 +981,17 @@ void ObjectList::show_context_menu(const bool evt_context_menu) const ItemType type = m_objects_model->GetItemType(item); if (!(type & (itObject | itVolume | itLayer | itInstance))) return; - - menu = type & itInstance ? plater->instance_menu() : - type & itLayer ? plater->layer_menu() : - m_objects_model->GetParent(item) != wxDataViewItem(nullptr) ? plater->part_menu() : - printer_technology() == ptFFF ? plater->object_menu() : plater->sla_object_menu(); + if (type & itVolume) { + int obj_idx, vol_idx; + get_selected_item_indexes(obj_idx, vol_idx, item); + if (obj_idx < 0 || vol_idx < 0) + return; + menu = object(obj_idx)->volumes[vol_idx]->text_configuration.has_value() ? plater->text_part_menu() : plater->part_menu(); + } + else + menu = type & itInstance ? plater->instance_menu() : + type & itLayer ? plater->layer_menu() : + printer_technology() == ptFFF ? plater->object_menu() : plater->sla_object_menu(); } else if (evt_context_menu) menu = plater->default_menu(); @@ -1749,7 +1768,7 @@ void ObjectList::load_shape_object(const std::string& type_name) // Create mesh BoundingBoxf3 bb; TriangleMesh mesh = create_mesh(type_name, bb); - load_mesh_object(mesh, _L("Shape") + "-" + _(type_name)); + load_mesh_object(mesh, _u8L("Shape") + "-" + type_name); #if ENABLE_RELOAD_FROM_DISK_REWORK if (!m_objects->empty()) m_objects->back()->volumes.front()->source.is_from_builtin_objects = true; @@ -1789,7 +1808,12 @@ void ObjectList::load_shape_object_from_gallery(const wxArrayString& input_files wxGetApp().mainframe->update_title(); } -void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center) +void ObjectList::load_mesh_object( + const TriangleMesh & mesh, + const std::string & name, + bool center, + const TextConfiguration *text_config /* = nullptr*/, + const Transform3d * transformation /* = nullptr*/) { // Add mesh to model as a new object Model& model = wxGetApp().plater()->model(); @@ -1799,22 +1823,29 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name #endif /* _DEBUG */ std::vector object_idxs; - auto bb = mesh.bounding_box(); ModelObject* new_object = model.add_object(); - new_object->name = into_u8(name); + new_object->name = name; new_object->add_instance(); // each object should have at list one instance ModelVolume* new_volume = new_object->add_volume(mesh); new_object->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); - new_volume->name = into_u8(name); + new_volume->name = name; + if (text_config) + new_volume->text_configuration = *text_config; // set a default extruder value, since user can't add it manually new_volume->config.set_key_value("extruder", new ConfigOptionInt(0)); new_object->invalidate_bounding_box(); - new_object->translate(-bb.center()); - - new_object->instances[0]->set_offset(center ? - to_3d(wxGetApp().plater()->build_volume().bounding_volume2d().center(), -new_object->origin_translation.z()) : + if (transformation) { + assert(!center); + Slic3r::Geometry::Transformation tr(*transformation); + new_object->instances[0]->set_transformation(tr); + } else { + auto bb = mesh.bounding_box(); + new_object->translate(-bb.center()); + new_object->instances[0]->set_offset( + center ? to_3d(wxGetApp().plater()->build_volume().bounding_volume2d().center(), -new_object->origin_translation.z()) : bb.center()); + } new_object->ensure_on_bed(); @@ -2898,6 +2929,7 @@ wxDataViewItemArray ObjectList::add_volumes_to_object_in_list(size_t obj_idx, st from_u8(volume->name), volume_idx, volume->type(), + volume->text_configuration.has_value(), get_warning_icon_name(volume->mesh().stats()), extruder2str(volume->config.has("extruder") ? volume->config.extruder() : 0)); add_settings_item(vol_item, &volume->config.get()); @@ -4160,14 +4192,14 @@ void ObjectList::change_part_type() } const bool is_cut_object = obj->is_cut(); - wxArrayString names; - names.Alloc(is_cut_object ? 3 : 5); if (!is_cut_object) for (const wxString& type : { _L("Part"), _L("Negative Volume") }) names.Add(type); - for (const wxString& type : { _L("Modifier"), _L("Support Blocker"), _L("Support Enforcer") } ) - names.Add(type); + names.Add(_L("Modifier")); + if (!volume->text_configuration.has_value()) + for (const wxString& name : { _L("Support Blocker"), _L("Support Enforcer") }) + names.Add(name); const int type_shift = is_cut_object ? 2 : 0; auto new_type = ModelVolumeType(type_shift + wxGetApp().GetSingleChoiceIndex(_L("Type:"), _L("Select type of part"), names, int(type) - type_shift)); diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index f98f091ddc..9cd3dc8e17 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -27,6 +27,7 @@ class ModelConfig; class ModelObject; class ModelVolume; class TriangleMesh; +struct TextConfiguration; enum class ModelVolumeType : int; // FIXME: broken build on mac os because of this is missing: @@ -257,7 +258,8 @@ public: void load_shape_object(const std::string &type_name); void load_shape_object_from_gallery(); void load_shape_object_from_gallery(const wxArrayString& input_files); - void load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center = true); + void load_mesh_object(const TriangleMesh &mesh, const std::string &name, bool center = true, + const TextConfiguration* text_config = nullptr, const Transform3d* transformation = nullptr); bool del_object(const int obj_idx); bool del_subobject_item(wxDataViewItem& item); void del_settings_from_config(const wxDataViewItem& parent_item); diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index aea771844a..bbcad29195 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -23,7 +23,7 @@ namespace GUI { const double ObjectManipulation::in_to_mm = 25.4; -const double ObjectManipulation::mm_to_in = 0.0393700787; +const double ObjectManipulation::mm_to_in = 1 / ObjectManipulation::in_to_mm; // Helper function to be used by drop to bed button. Returns lowest point of this // volume in world coordinate system. diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index 4cc3a1f2b0..f4200f8343 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -1,537 +1,542 @@ -#include "GLGizmoBase.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" - -#include - -#include "slic3r/GUI/GUI_App.hpp" -#include "slic3r/GUI/GUI_ObjectManipulation.hpp" -#if ENABLE_LEGACY_OPENGL_REMOVAL -#include "slic3r/GUI/Plater.hpp" -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -// TODO: Display tooltips quicker on Linux - -namespace Slic3r { -namespace GUI { - -const float GLGizmoBase::Grabber::SizeFactor = 0.05f; -const float GLGizmoBase::Grabber::MinHalfSize = 1.5f; -const float GLGizmoBase::Grabber::DraggingScaleFactor = 1.25f; - -#if ENABLE_RAYCAST_PICKING -PickingModel GLGizmoBase::Grabber::s_cube; -PickingModel GLGizmoBase::Grabber::s_cone; -#else -GLModel GLGizmoBase::Grabber::s_cube; -GLModel GLGizmoBase::Grabber::s_cone; -#endif // ENABLE_RAYCAST_PICKING - -GLGizmoBase::Grabber::~Grabber() -{ -#if ENABLE_RAYCAST_PICKING - if (s_cube.model.is_initialized()) - s_cube.model.reset(); - - if (s_cone.model.is_initialized()) - s_cone.model.reset(); -#else - if (s_cube.is_initialized()) - s_cube.reset(); - - if (s_cone.is_initialized()) - s_cone.reset(); -#endif // ENABLE_RAYCAST_PICKING -} - -float GLGizmoBase::Grabber::get_half_size(float size) const -{ - return std::max(size * SizeFactor, MinHalfSize); -} - -float GLGizmoBase::Grabber::get_dragging_half_size(float size) const -{ - return get_half_size(size) * DraggingScaleFactor; -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoBase::Grabber::register_raycasters_for_picking(int id) -{ - picking_id = id; - // registration will happen on next call to render() -} - -void GLGizmoBase::Grabber::unregister_raycasters_for_picking() -{ - wxGetApp().plater()->canvas3D()->remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, picking_id); - picking_id = -1; - raycasters = { nullptr }; -} -#endif // ENABLE_RAYCAST_PICKING - -#if ENABLE_RAYCAST_PICKING -void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color) -#else -void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color, bool picking) -#endif // ENABLE_RAYCAST_PICKING -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_current_shader(); - if (shader == nullptr) - return; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_RAYCAST_PICKING - if (!s_cube.model.is_initialized()) { -#else - if (!s_cube.is_initialized()) { -#endif // ENABLE_RAYCAST_PICKING - // This cannot be done in constructor, OpenGL is not yet - // initialized at that point (on Linux at least). - indexed_triangle_set its = its_make_cube(1.0, 1.0, 1.0); - its_translate(its, -0.5f * Vec3f::Ones()); -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - s_cube.model.init_from(its); - s_cube.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); -#else - s_cube.init_from(its); -#endif // ENABLE_RAYCAST_PICKING -#else - s_cube.init_from(its, BoundingBoxf3{ { -0.5, -0.5, -0.5 }, { 0.5, 0.5, 0.5 } }); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - -#if ENABLE_RAYCAST_PICKING - if (!s_cone.model.is_initialized()) { - indexed_triangle_set its = its_make_cone(0.375, 1.5, double(PI) / 18.0); - s_cone.model.init_from(its); - s_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); - } -#else - if (!s_cone.is_initialized()) - s_cone.init_from(its_make_cone(0.375, 1.5, double(PI) / 18.0)); -#endif // ENABLE_RAYCAST_PICKING - - const float half_size = dragging ? get_dragging_half_size(size) : get_half_size(size); - -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - s_cube.model.set_color(render_color); - s_cone.model.set_color(render_color); -#else - s_cube.set_color(render_color); - s_cone.set_color(render_color); -#endif // ENABLE_RAYCAST_PICKING - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_RAYCAST_PICKING - const Transform3d& view_matrix = camera.get_view_matrix(); - const Matrix3d view_matrix_no_offset = view_matrix.matrix().block(0, 0, 3, 3); - std::vector elements_matrices(GRABBER_ELEMENTS_MAX_COUNT, Transform3d::Identity()); - elements_matrices[0] = matrix * Geometry::translation_transform(center) * Geometry::rotation_transform(angles) * Geometry::scale_transform(2.0 * half_size); - Transform3d view_model_matrix = view_matrix * elements_matrices[0]; -#else - const Transform3d& view_matrix = camera.get_view_matrix(); - const Transform3d model_matrix = matrix * Geometry::translation_transform(center) * Geometry::rotation_transform(angles) * Geometry::scale_transform(2.0 * half_size); - const Transform3d view_model_matrix = view_matrix * model_matrix; -#endif // ENABLE_RAYCAST_PICKING - - shader->set_uniform("view_model_matrix", view_model_matrix); -#if ENABLE_RAYCAST_PICKING - Matrix3d view_normal_matrix = view_matrix_no_offset * elements_matrices[0].matrix().block(0, 0, 3, 3).inverse().transpose(); -#else - const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); -#endif // ENABLE_RAYCAST_PICKING - shader->set_uniform("view_normal_matrix", view_normal_matrix); -#else - s_cube.set_color(-1, render_color); - s_cone.set_color(-1, render_color); - glsafe(::glPushMatrix()); - glsafe(::glTranslated(center.x(), center.y(), center.z())); - glsafe(::glRotated(Geometry::rad2deg(angles.z()), 0.0, 0.0, 1.0)); - glsafe(::glRotated(Geometry::rad2deg(angles.y()), 0.0, 1.0, 0.0)); - glsafe(::glRotated(Geometry::rad2deg(angles.x()), 1.0, 0.0, 0.0)); - glsafe(::glScaled(2.0 * half_size, 2.0 * half_size, 2.0 * half_size)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_RAYCAST_PICKING - s_cube.model.render(); - - auto render_extension = [&view_matrix, &view_matrix_no_offset, shader](const Transform3d& matrix) { - const Transform3d view_model_matrix = view_matrix * matrix; - shader->set_uniform("view_model_matrix", view_model_matrix); - const Matrix3d view_normal_matrix = view_matrix_no_offset * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader->set_uniform("view_normal_matrix", view_normal_matrix); - s_cone.model.render(); - }; -#else - s_cube.render(); -#endif // ENABLE_RAYCAST_PICKING - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[1] = elements_matrices[0] * Geometry::translation_transform(Vec3d::UnitX()) * Geometry::rotation_transform({ 0.0, 0.5 * double(PI), 0.0 }); - render_extension(elements_matrices[1]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::translation_transform(Vec3d::UnitX()) * Geometry::rotation_transform({ 0.0, 0.5 * double(PI), 0.0 })); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[2] = elements_matrices[0] * Geometry::translation_transform(-Vec3d::UnitX()) * Geometry::rotation_transform({ 0.0, -0.5 * double(PI), 0.0 }); - render_extension(elements_matrices[2]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::translation_transform(-Vec3d::UnitX()) * Geometry::rotation_transform({ 0.0, -0.5 * double(PI), 0.0 })); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[3] = elements_matrices[0] * Geometry::translation_transform(Vec3d::UnitY()) * Geometry::rotation_transform({ -0.5 * double(PI), 0.0, 0.0 }); - render_extension(elements_matrices[3]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::translation_transform(Vec3d::UnitY()) * Geometry::rotation_transform({ -0.5 * double(PI), 0.0, 0.0 })); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[4] = elements_matrices[0] * Geometry::translation_transform(-Vec3d::UnitY()) * Geometry::rotation_transform({ 0.5 * double(PI), 0.0, 0.0 }); - render_extension(elements_matrices[4]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix* Geometry::translation_transform(-Vec3d::UnitY())* Geometry::rotation_transform({ 0.5 * double(PI), 0.0, 0.0 })); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[5] = elements_matrices[0] * Geometry::translation_transform(Vec3d::UnitZ()); - render_extension(elements_matrices[5]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix* Geometry::translation_transform(Vec3d::UnitZ())); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { -#if ENABLE_RAYCAST_PICKING - elements_matrices[6] = elements_matrices[0] * Geometry::translation_transform(-Vec3d::UnitZ()) * Geometry::rotation_transform({ double(PI), 0.0, 0.0 }); - render_extension(elements_matrices[6]); -#else - shader->set_uniform("view_model_matrix", view_model_matrix* Geometry::translation_transform(-Vec3d::UnitZ())* Geometry::rotation_transform({ double(PI), 0.0, 0.0 })); - s_cone.render(); -#endif // ENABLE_RAYCAST_PICKING - } -#else - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(1.0, 0.0, 0.0)); - glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(-1.0, 0.0, 0.0)); - glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, 1.0, 0.0)); - glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, -1.0, 0.0)); - glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, 0.0, 1.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { - glsafe(::glPushMatrix()); - glsafe(::glTranslated(0.0, 0.0, -1.0)); - glsafe(::glRotated(Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); - s_cone.render(); - glsafe(::glPopMatrix()); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_RAYCAST_PICKING - if (raycasters[0] == nullptr) { - GLCanvas3D& canvas = *wxGetApp().plater()->canvas3D(); - raycasters[0] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cube.mesh_raycaster, elements_matrices[0]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) - raycasters[1] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[1]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) - raycasters[2] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[2]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) - raycasters[3] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[3]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) - raycasters[4] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[4]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) - raycasters[5] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[5]); - if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) - raycasters[6] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[6]); - } - else { - for (size_t i = 0; i < GRABBER_ELEMENTS_MAX_COUNT; ++i) { - if (raycasters[i] != nullptr) - raycasters[i]->set_transform(elements_matrices[i]); - } - } -#endif // ENABLE_RAYCAST_PICKING -} - -GLGizmoBase::GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : m_parent(parent) - , m_icon_filename(icon_filename) - , m_sprite_id(sprite_id) - , m_imgui(wxGetApp().imgui()) -{ -} - -void GLGizmoBase::set_hover_id(int id) -{ - // do not change hover id during dragging - assert(!m_dragging); - - // allow empty grabbers when not using grabbers but use hover_id - flatten, rotate -// if (!m_grabbers.empty() && id >= (int) m_grabbers.size()) -// return; - - m_hover_id = id; - on_set_hover_id(); -} - -bool GLGizmoBase::update_items_state() -{ - bool res = m_dirty; - m_dirty = false; - return res; -} - -#if ENABLE_RAYCAST_PICKING -void GLGizmoBase::register_grabbers_for_picking() -{ - for (size_t i = 0; i < m_grabbers.size(); ++i) { - m_grabbers[i].register_raycasters_for_picking((m_group_id >= 0) ? m_group_id : i); - } -} - -void GLGizmoBase::unregister_grabbers_for_picking() -{ - for (size_t i = 0; i < m_grabbers.size(); ++i) { - m_grabbers[i].unregister_raycasters_for_picking(); - } -} -#else -ColorRGBA GLGizmoBase::picking_color_component(unsigned int id) const -{ - id = BASE_ID - id; - if (m_group_id > -1) - id -= m_group_id; - - return picking_decode(id); -} -#endif // ENABLE_RAYCAST_PICKING - -void GLGizmoBase::render_grabbers(const BoundingBoxf3& box) const -{ - render_grabbers((float)((box.size().x() + box.size().y() + box.size().z()) / 3.0)); -} - -void GLGizmoBase::render_grabbers(float size) const -{ - GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); - if (shader == nullptr) - return; - shader->start_using(); - shader->set_uniform("emission_factor", 0.1f); - for (int i = 0; i < (int)m_grabbers.size(); ++i) { - if (m_grabbers[i].enabled) - m_grabbers[i].render(m_hover_id == i, size); - } - shader->stop_using(); -} - -#if !ENABLE_RAYCAST_PICKING -void GLGizmoBase::render_grabbers_for_picking(const BoundingBoxf3& box) const -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader != nullptr) { - shader->start_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - const float mean_size = float((box.size().x() + box.size().y() + box.size().z()) / 3.0); - - for (unsigned int i = 0; i < (unsigned int)m_grabbers.size(); ++i) { - if (m_grabbers[i].enabled) { - m_grabbers[i].color = picking_color_component(i); - m_grabbers[i].render_for_picking(mean_size); - } - } -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} -#endif // !ENABLE_RAYCAST_PICKING - -// help function to process grabbers -// call start_dragging, stop_dragging, on_dragging -bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { - bool is_dragging_finished = false; - if (mouse_event.Moving()) { - // it should not happen but for sure - assert(!m_dragging); - if (m_dragging) is_dragging_finished = true; - else return false; - } - - if (mouse_event.LeftDown()) { - Selection &selection = m_parent.get_selection(); - if (!selection.is_empty() && m_hover_id != -1 /* && - (m_grabbers.empty() || m_hover_id < static_cast(m_grabbers.size()))*/) { - selection.setup_cache(); - - m_dragging = true; - for (auto &grabber : m_grabbers) grabber.dragging = false; -// if (!m_grabbers.empty() && m_hover_id < int(m_grabbers.size())) -// m_grabbers[m_hover_id].dragging = true; - - on_start_dragging(); - - // Let the plater know that the dragging started - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED)); - m_parent.set_as_dirty(); - return true; - } - } else if (m_dragging) { - // when mouse cursor leave window than finish actual dragging operation - bool is_leaving = mouse_event.Leaving(); - if (mouse_event.Dragging()) { - Point mouse_coord(mouse_event.GetX(), mouse_event.GetY()); - auto ray = m_parent.mouse_ray(mouse_coord); - UpdateData data(ray, mouse_coord); - - on_dragging(data); - - wxGetApp().obj_manipul()->set_dirty(); - m_parent.set_as_dirty(); - return true; - } - else if (mouse_event.LeftUp() || is_leaving || is_dragging_finished) { -#if ENABLE_WORLD_COORDINATE - do_stop_dragging(is_leaving); -#else - for (auto &grabber : m_grabbers) grabber.dragging = false; - m_dragging = false; - - // NOTE: This should be part of GLCanvas3D - // Reset hover_id when leave window - if (is_leaving) m_parent.mouse_up_cleanup(); - - on_stop_dragging(); - - // There is prediction that after draggign, data are changed - // Data are updated twice also by canvas3D::reload_scene. - // Should be fixed. - m_parent.get_gizmos_manager().update_data(); - - wxGetApp().obj_manipul()->set_dirty(); - - // Let the plater know that the dragging finished, so a delayed - // refresh of the scene with the background processing data should - // be performed. - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); - // updates camera target constraints - m_parent.refresh_camera_scene_box(); -#endif // ENABLE_WORLD_COORDINATE - return true; - } - } - return false; -} - -#if ENABLE_WORLD_COORDINATE -void GLGizmoBase::do_stop_dragging(bool perform_mouse_cleanup) -{ - for (auto& grabber : m_grabbers) grabber.dragging = false; - m_dragging = false; - - // NOTE: This should be part of GLCanvas3D - // Reset hover_id when leave window - if (perform_mouse_cleanup) m_parent.mouse_up_cleanup(); - - on_stop_dragging(); - - // There is prediction that after draggign, data are changed - // Data are updated twice also by canvas3D::reload_scene. - // Should be fixed. - m_parent.get_gizmos_manager().update_data(); - - wxGetApp().obj_manipul()->set_dirty(); - - // Let the plater know that the dragging finished, so a delayed - // refresh of the scene with the background processing data should - // be performed. - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); - // updates camera target constraints - m_parent.refresh_camera_scene_box(); -} -#endif // ENABLE_WORLD_COORDINATE - -std::string GLGizmoBase::format(float value, unsigned int decimals) const -{ - return Slic3r::string_printf("%.*f", decimals, value); -} - -void GLGizmoBase::set_dirty() { - m_dirty = true; -} - - - -void GLGizmoBase::render_input_window(float x, float y, float bottom_limit) -{ - on_render_input_window(x, y, bottom_limit); - if (m_first_input_window_render) { - // imgui windows that don't have an initial size needs to be processed once to get one - // and are not rendered in the first frame - // so, we forces to render another frame the first time the imgui window is shown - // https://github.com/ocornut/imgui/issues/2949 - m_parent.set_as_dirty(); - m_parent.request_extra_frame(); - m_first_input_window_render = false; - } -} - - - -std::string GLGizmoBase::get_name(bool include_shortcut) const -{ - int key = get_shortcut_key(); +#include "GLGizmoBase.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" + +#include + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#if ENABLE_LEGACY_OPENGL_REMOVAL +#include "slic3r/GUI/Plater.hpp" +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +// TODO: Display tooltips quicker on Linux + +namespace Slic3r { +namespace GUI { + +const float GLGizmoBase::Grabber::SizeFactor = 0.05f; +const float GLGizmoBase::Grabber::MinHalfSize = 1.5f; +const float GLGizmoBase::Grabber::DraggingScaleFactor = 1.25f; + +#if ENABLE_RAYCAST_PICKING +PickingModel GLGizmoBase::Grabber::s_cube; +PickingModel GLGizmoBase::Grabber::s_cone; +#else +GLModel GLGizmoBase::Grabber::s_cube; +GLModel GLGizmoBase::Grabber::s_cone; +#endif // ENABLE_RAYCAST_PICKING + +GLGizmoBase::Grabber::~Grabber() +{ +#if ENABLE_RAYCAST_PICKING + if (s_cube.model.is_initialized()) + s_cube.model.reset(); + + if (s_cone.model.is_initialized()) + s_cone.model.reset(); +#else + if (s_cube.is_initialized()) + s_cube.reset(); + + if (s_cone.is_initialized()) + s_cone.reset(); +#endif // ENABLE_RAYCAST_PICKING +} + +float GLGizmoBase::Grabber::get_half_size(float size) const +{ + return std::max(size * SizeFactor, MinHalfSize); +} + +float GLGizmoBase::Grabber::get_dragging_half_size(float size) const +{ + return get_half_size(size) * DraggingScaleFactor; +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoBase::Grabber::register_raycasters_for_picking(int id) +{ + picking_id = id; + // registration will happen on next call to render() +} + +void GLGizmoBase::Grabber::unregister_raycasters_for_picking() +{ + wxGetApp().plater()->canvas3D()->remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo, picking_id); + picking_id = -1; + raycasters = { nullptr }; +} +#endif // ENABLE_RAYCAST_PICKING + +#if ENABLE_RAYCAST_PICKING +void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color) +#else +void GLGizmoBase::Grabber::render(float size, const ColorRGBA& render_color, bool picking) +#endif // ENABLE_RAYCAST_PICKING +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_current_shader(); + if (shader == nullptr) + return; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_RAYCAST_PICKING + if (!s_cube.model.is_initialized()) { +#else + if (!s_cube.is_initialized()) { +#endif // ENABLE_RAYCAST_PICKING + // This cannot be done in constructor, OpenGL is not yet + // initialized at that point (on Linux at least). + indexed_triangle_set its = its_make_cube(1.0, 1.0, 1.0); + its_translate(its, -0.5f * Vec3f::Ones()); +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + s_cube.model.init_from(its); + s_cube.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); +#else + s_cube.init_from(its); +#endif // ENABLE_RAYCAST_PICKING +#else + s_cube.init_from(its, BoundingBoxf3{ { -0.5, -0.5, -0.5 }, { 0.5, 0.5, 0.5 } }); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + +#if ENABLE_RAYCAST_PICKING + if (!s_cone.model.is_initialized()) { + indexed_triangle_set its = its_make_cone(0.375, 1.5, double(PI) / 18.0); + s_cone.model.init_from(its); + s_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); + } +#else + if (!s_cone.is_initialized()) + s_cone.init_from(its_make_cone(0.375, 1.5, double(PI) / 18.0)); +#endif // ENABLE_RAYCAST_PICKING + + const float half_size = dragging ? get_dragging_half_size(size) : get_half_size(size); + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + s_cube.model.set_color(render_color); + s_cone.model.set_color(render_color); +#else + s_cube.set_color(render_color); + s_cone.set_color(render_color); +#endif // ENABLE_RAYCAST_PICKING + + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_RAYCAST_PICKING + const Transform3d& view_matrix = camera.get_view_matrix(); + const Matrix3d view_matrix_no_offset = view_matrix.matrix().block(0, 0, 3, 3); + std::vector elements_matrices(GRABBER_ELEMENTS_MAX_COUNT, Transform3d::Identity()); + elements_matrices[0] = matrix * Geometry::translation_transform(center) * Geometry::rotation_transform(angles) * Geometry::scale_transform(2.0 * half_size); + Transform3d view_model_matrix = view_matrix * elements_matrices[0]; +#else + const Transform3d& view_matrix = camera.get_view_matrix(); + const Transform3d model_matrix = matrix * Geometry::translation_transform(center) * Geometry::rotation_transform(angles) * Geometry::scale_transform(2.0 * half_size); + const Transform3d view_model_matrix = view_matrix * model_matrix; +#endif // ENABLE_RAYCAST_PICKING + + shader->set_uniform("view_model_matrix", view_model_matrix); +#if ENABLE_RAYCAST_PICKING + Matrix3d view_normal_matrix = view_matrix_no_offset * elements_matrices[0].matrix().block(0, 0, 3, 3).inverse().transpose(); +#else + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); +#endif // ENABLE_RAYCAST_PICKING + shader->set_uniform("view_normal_matrix", view_normal_matrix); +#else + s_cube.set_color(-1, render_color); + s_cone.set_color(-1, render_color); + glsafe(::glPushMatrix()); + glsafe(::glTranslated(center.x(), center.y(), center.z())); + glsafe(::glRotated(Geometry::rad2deg(angles.z()), 0.0, 0.0, 1.0)); + glsafe(::glRotated(Geometry::rad2deg(angles.y()), 0.0, 1.0, 0.0)); + glsafe(::glRotated(Geometry::rad2deg(angles.x()), 1.0, 0.0, 0.0)); + glsafe(::glScaled(2.0 * half_size, 2.0 * half_size, 2.0 * half_size)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_RAYCAST_PICKING + s_cube.model.render(); + + auto render_extension = [&view_matrix, &view_matrix_no_offset, shader](const Transform3d& matrix) { + const Transform3d view_model_matrix = view_matrix * matrix; + shader->set_uniform("view_model_matrix", view_model_matrix); + const Matrix3d view_normal_matrix = view_matrix_no_offset * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader->set_uniform("view_normal_matrix", view_normal_matrix); + s_cone.model.render(); + }; +#else + s_cube.render(); +#endif // ENABLE_RAYCAST_PICKING + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[1] = elements_matrices[0] * Geometry::translation_transform(Vec3d::UnitX()) * Geometry::rotation_transform({ 0.0, 0.5 * double(PI), 0.0 }); + render_extension(elements_matrices[1]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::translation_transform(Vec3d::UnitX()) * Geometry::rotation_transform({ 0.0, 0.5 * double(PI), 0.0 })); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[2] = elements_matrices[0] * Geometry::translation_transform(-Vec3d::UnitX()) * Geometry::rotation_transform({ 0.0, -0.5 * double(PI), 0.0 }); + render_extension(elements_matrices[2]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::translation_transform(-Vec3d::UnitX()) * Geometry::rotation_transform({ 0.0, -0.5 * double(PI), 0.0 })); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[3] = elements_matrices[0] * Geometry::translation_transform(Vec3d::UnitY()) * Geometry::rotation_transform({ -0.5 * double(PI), 0.0, 0.0 }); + render_extension(elements_matrices[3]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix * Geometry::translation_transform(Vec3d::UnitY()) * Geometry::rotation_transform({ -0.5 * double(PI), 0.0, 0.0 })); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[4] = elements_matrices[0] * Geometry::translation_transform(-Vec3d::UnitY()) * Geometry::rotation_transform({ 0.5 * double(PI), 0.0, 0.0 }); + render_extension(elements_matrices[4]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix* Geometry::translation_transform(-Vec3d::UnitY())* Geometry::rotation_transform({ 0.5 * double(PI), 0.0, 0.0 })); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[5] = elements_matrices[0] * Geometry::translation_transform(Vec3d::UnitZ()); + render_extension(elements_matrices[5]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix* Geometry::translation_transform(Vec3d::UnitZ())); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { +#if ENABLE_RAYCAST_PICKING + elements_matrices[6] = elements_matrices[0] * Geometry::translation_transform(-Vec3d::UnitZ()) * Geometry::rotation_transform({ double(PI), 0.0, 0.0 }); + render_extension(elements_matrices[6]); +#else + shader->set_uniform("view_model_matrix", view_model_matrix* Geometry::translation_transform(-Vec3d::UnitZ())* Geometry::rotation_transform({ double(PI), 0.0, 0.0 })); + s_cone.render(); +#endif // ENABLE_RAYCAST_PICKING + } +#else + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(1.0, 0.0, 0.0)); + glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(-1.0, 0.0, 0.0)); + glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 0.0, 1.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 1.0, 0.0)); + glsafe(::glRotated(-0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, -1.0, 0.0)); + glsafe(::glRotated(0.5 * Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 0.0, 1.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) { + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 0.0, -1.0)); + glsafe(::glRotated(Geometry::rad2deg(double(PI)), 1.0, 0.0, 0.0)); + s_cone.render(); + glsafe(::glPopMatrix()); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_RAYCAST_PICKING + if (raycasters[0] == nullptr) { + GLCanvas3D& canvas = *wxGetApp().plater()->canvas3D(); + raycasters[0] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cube.mesh_raycaster, elements_matrices[0]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosX)) != 0) + raycasters[1] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[1]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegX)) != 0) + raycasters[2] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[2]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosY)) != 0) + raycasters[3] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[3]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegY)) != 0) + raycasters[4] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[4]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::PosZ)) != 0) + raycasters[5] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[5]); + if ((int(extensions) & int(GLGizmoBase::EGrabberExtension::NegZ)) != 0) + raycasters[6] = canvas.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, picking_id, *s_cone.mesh_raycaster, elements_matrices[6]); + } + else { + for (size_t i = 0; i < GRABBER_ELEMENTS_MAX_COUNT; ++i) { + if (raycasters[i] != nullptr) + raycasters[i]->set_transform(elements_matrices[i]); + } + } +#endif // ENABLE_RAYCAST_PICKING +} + +GLGizmoBase::GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : m_parent(parent) + , m_group_id(-1) + , m_state(Off) + , m_shortcut_key(NO_SHORTCUT_KEY_VALUE) + , m_icon_filename(icon_filename) + , m_sprite_id(sprite_id) + , m_imgui(wxGetApp().imgui()) +{ +} + +void GLGizmoBase::set_hover_id(int id) +{ + // do not change hover id during dragging + assert(!m_dragging); + + // allow empty grabbers when not using grabbers but use hover_id - flatten, rotate +// if (!m_grabbers.empty() && id >= (int) m_grabbers.size()) +// return; + + m_hover_id = id; + on_set_hover_id(); +} + +bool GLGizmoBase::update_items_state() +{ + bool res = m_dirty; + m_dirty = false; + return res; +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoBase::register_grabbers_for_picking() +{ + for (size_t i = 0; i < m_grabbers.size(); ++i) { + m_grabbers[i].register_raycasters_for_picking((m_group_id >= 0) ? m_group_id : i); + } +} + +void GLGizmoBase::unregister_grabbers_for_picking() +{ + for (size_t i = 0; i < m_grabbers.size(); ++i) { + m_grabbers[i].unregister_raycasters_for_picking(); + } +} +#else +ColorRGBA GLGizmoBase::picking_color_component(unsigned int id) const +{ + id = BASE_ID - id; + if (m_group_id > -1) + id -= m_group_id; + + return picking_decode(id); +} +#endif // ENABLE_RAYCAST_PICKING + +void GLGizmoBase::render_grabbers(const BoundingBoxf3& box) const +{ + render_grabbers((float)((box.size().x() + box.size().y() + box.size().z()) / 3.0)); +} + +void GLGizmoBase::render_grabbers(float size) const +{ + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + shader->start_using(); + shader->set_uniform("emission_factor", 0.1f); + for (int i = 0; i < (int)m_grabbers.size(); ++i) { + if (m_grabbers[i].enabled) + m_grabbers[i].render(m_hover_id == i, size); + } + shader->stop_using(); +} + +#if !ENABLE_RAYCAST_PICKING +void GLGizmoBase::render_grabbers_for_picking(const BoundingBoxf3& box) const +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader != nullptr) { + shader->start_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + const float mean_size = float((box.size().x() + box.size().y() + box.size().z()) / 3.0); + + for (unsigned int i = 0; i < (unsigned int)m_grabbers.size(); ++i) { + if (m_grabbers[i].enabled) { + m_grabbers[i].color = picking_color_component(i); + m_grabbers[i].render_for_picking(mean_size); + } + } +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} +#endif // !ENABLE_RAYCAST_PICKING + +// help function to process grabbers +// call start_dragging, stop_dragging, on_dragging +bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { + bool is_dragging_finished = false; + if (mouse_event.Moving()) { + // it should not happen but for sure + assert(!m_dragging); + if (m_dragging) is_dragging_finished = true; + else return false; + } + + if (mouse_event.LeftDown()) { + Selection &selection = m_parent.get_selection(); + if (!selection.is_empty() && m_hover_id != -1 /* && + (m_grabbers.empty() || m_hover_id < static_cast(m_grabbers.size()))*/) { + selection.setup_cache(); + + m_dragging = true; + for (auto &grabber : m_grabbers) grabber.dragging = false; +// if (!m_grabbers.empty() && m_hover_id < int(m_grabbers.size())) +// m_grabbers[m_hover_id].dragging = true; + + on_start_dragging(); + + // Let the plater know that the dragging started + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED)); + m_parent.set_as_dirty(); + return true; + } + } else if (m_dragging) { + // when mouse cursor leave window than finish actual dragging operation + bool is_leaving = mouse_event.Leaving(); + if (mouse_event.Dragging()) { + Point mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + auto ray = m_parent.mouse_ray(mouse_coord); + UpdateData data(ray, mouse_coord); + + on_dragging(data); + + wxGetApp().obj_manipul()->set_dirty(); + m_parent.set_as_dirty(); + return true; + } + else if (mouse_event.LeftUp() || is_leaving || is_dragging_finished) { +#if ENABLE_WORLD_COORDINATE + do_stop_dragging(is_leaving); +#else + for (auto &grabber : m_grabbers) grabber.dragging = false; + m_dragging = false; + + // NOTE: This should be part of GLCanvas3D + // Reset hover_id when leave window + if (is_leaving) m_parent.mouse_up_cleanup(); + + on_stop_dragging(); + + // There is prediction that after draggign, data are changed + // Data are updated twice also by canvas3D::reload_scene. + // Should be fixed. + m_parent.get_gizmos_manager().update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + + // Let the plater know that the dragging finished, so a delayed + // refresh of the scene with the background processing data should + // be performed. + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + m_parent.refresh_camera_scene_box(); +#endif // ENABLE_WORLD_COORDINATE + return true; + } + } + return false; +} + +#if ENABLE_WORLD_COORDINATE +void GLGizmoBase::do_stop_dragging(bool perform_mouse_cleanup) +{ + for (auto& grabber : m_grabbers) grabber.dragging = false; + m_dragging = false; + + // NOTE: This should be part of GLCanvas3D + // Reset hover_id when leave window + if (perform_mouse_cleanup) m_parent.mouse_up_cleanup(); + + on_stop_dragging(); + + // There is prediction that after draggign, data are changed + // Data are updated twice also by canvas3D::reload_scene. + // Should be fixed. + m_parent.get_gizmos_manager().update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + + // Let the plater know that the dragging finished, so a delayed + // refresh of the scene with the background processing data should + // be performed. + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + m_parent.refresh_camera_scene_box(); +} +#endif // ENABLE_WORLD_COORDINATE + +std::string GLGizmoBase::format(float value, unsigned int decimals) const +{ + return Slic3r::string_printf("%.*f", decimals, value); +} + +void GLGizmoBase::set_dirty() { + m_dirty = true; +} + + + +void GLGizmoBase::render_input_window(float x, float y, float bottom_limit) +{ + on_render_input_window(x, y, bottom_limit); + if (m_first_input_window_render) { + // imgui windows that don't have an initial size needs to be processed once to get one + // and are not rendered in the first frame + // so, we forces to render another frame the first time the imgui window is shown + // https://github.com/ocornut/imgui/issues/2949 + m_parent.set_as_dirty(); + m_parent.request_extra_frame(); + m_first_input_window_render = false; + } +} + + + +std::string GLGizmoBase::get_name(bool include_shortcut) const +{ std::string out = on_get_name(); - if (include_shortcut && key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z) - out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]"; - return out; -} - - -} // namespace GUI -} // namespace Slic3r + if (!include_shortcut) return out; + int key = get_shortcut_key(); + assert(key==NO_SHORTCUT_KEY_VALUE || (key >= WXK_CONTROL_A && key <= WXK_CONTROL_Z)); + out += std::string(" [") + char(int('A') + key - int(WXK_CONTROL_A)) + "]"; + return out; +} + + +} // namespace GUI +} // namespace Slic3r + diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index 43624964a8..1d134948d4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -1,113 +1,116 @@ -#ifndef slic3r_GLGizmoBase_hpp_ -#define slic3r_GLGizmoBase_hpp_ - -#include "libslic3r/Point.hpp" -#include "libslic3r/Color.hpp" - -#include "slic3r/GUI/I18N.hpp" -#include "slic3r/GUI/GLModel.hpp" -#if ENABLE_RAYCAST_PICKING -#include "slic3r/GUI/MeshUtils.hpp" -#include "slic3r/GUI/SceneRaycaster.hpp" -#endif // ENABLE_RAYCAST_PICKING - -#include - -class wxWindow; -class wxMouseEvent; - -namespace Slic3r { - -class BoundingBoxf3; -class Linef3; -class ModelObject; - -namespace GUI { - -static const ColorRGBA DEFAULT_BASE_COLOR = { 0.625f, 0.625f, 0.625f, 1.0f }; -static const ColorRGBA DEFAULT_DRAG_COLOR = ColorRGBA::WHITE(); -static const ColorRGBA DEFAULT_HIGHLIGHT_COLOR = ColorRGBA::ORANGE(); -static const std::array AXES_COLOR = {{ ColorRGBA::X(), ColorRGBA::Y(), ColorRGBA::Z() }}; -static const ColorRGBA CONSTRAINED_COLOR = ColorRGBA::GRAY(); - -class ImGuiWrapper; -class GLCanvas3D; -enum class CommonGizmosDataID; -class CommonGizmosDataPool; - -class GLGizmoBase -{ -public: - // Starting value for ids to avoid clashing with ids used by GLVolumes - // (254 is choosen to leave some space for forward compatibility) - static const unsigned int BASE_ID = 255 * 255 * 254; - static const unsigned int GRABBER_ELEMENTS_MAX_COUNT = 7; - - enum class EGrabberExtension - { - None = 0, - PosX = 1 << 0, - NegX = 1 << 1, - PosY = 1 << 2, - NegY = 1 << 3, - PosZ = 1 << 4, - NegZ = 1 << 5, - }; - -protected: - struct Grabber - { - static const float SizeFactor; - static const float MinHalfSize; - static const float DraggingScaleFactor; - - bool enabled{ true }; - bool dragging{ false }; - Vec3d center{ Vec3d::Zero() }; - Vec3d angles{ Vec3d::Zero() }; -#if ENABLE_LEGACY_OPENGL_REMOVAL - Transform3d matrix{ Transform3d::Identity() }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - ColorRGBA color{ ColorRGBA::WHITE() }; - EGrabberExtension extensions{ EGrabberExtension::None }; -#if ENABLE_RAYCAST_PICKING - // the picking id shared by all the elements - int picking_id{ -1 }; - std::array, GRABBER_ELEMENTS_MAX_COUNT> raycasters = { nullptr }; -#endif // ENABLE_RAYCAST_PICKING - - Grabber() = default; - ~Grabber(); - -#if ENABLE_RAYCAST_PICKING - void render(bool hover, float size) { render(size, hover ? complementary(color) : color); } -#else - void render(bool hover, float size) { render(size, hover ? complementary(color) : color, false); } - void render_for_picking(float size) { render(size, color, true); } -#endif // ENABLE_RAYCAST_PICKING - - float get_half_size(float size) const; - float get_dragging_half_size(float size) const; - -#if ENABLE_RAYCAST_PICKING - void register_raycasters_for_picking(int id); - void unregister_raycasters_for_picking(); -#endif // ENABLE_RAYCAST_PICKING - - private: -#if ENABLE_RAYCAST_PICKING - void render(float size, const ColorRGBA& render_color); -#else - void render(float size, const ColorRGBA& render_color, bool picking); -#endif // ENABLE_RAYCAST_PICKING - -#if ENABLE_RAYCAST_PICKING - static PickingModel s_cube; - static PickingModel s_cone; -#else - static GLModel s_cube; - static GLModel s_cone; -#endif // ENABLE_RAYCAST_PICKING +#ifndef slic3r_GLGizmoBase_hpp_ +#define slic3r_GLGizmoBase_hpp_ + +#include "libslic3r/Point.hpp" +#include "libslic3r/Color.hpp" + +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/GLModel.hpp" +#if ENABLE_RAYCAST_PICKING +#include "slic3r/GUI/MeshUtils.hpp" +#include "slic3r/GUI/SceneRaycaster.hpp" +#endif // ENABLE_RAYCAST_PICKING + +#include + +class wxWindow; +class wxMouseEvent; + +namespace Slic3r { + +class BoundingBoxf3; +class Linef3; +class ModelObject; + +namespace GUI { + +static const ColorRGBA DEFAULT_BASE_COLOR = { 0.625f, 0.625f, 0.625f, 1.0f }; +static const ColorRGBA DEFAULT_DRAG_COLOR = ColorRGBA::WHITE(); +static const ColorRGBA DEFAULT_HIGHLIGHT_COLOR = ColorRGBA::ORANGE(); +static const std::array AXES_COLOR = {{ ColorRGBA::X(), ColorRGBA::Y(), ColorRGBA::Z() }}; +static const ColorRGBA CONSTRAINED_COLOR = ColorRGBA::GRAY(); + +class ImGuiWrapper; +class GLCanvas3D; +enum class CommonGizmosDataID; +class CommonGizmosDataPool; + +class GLGizmoBase +{ +public: + // Starting value for ids to avoid clashing with ids used by GLVolumes + // (254 is choosen to leave some space for forward compatibility) + static const unsigned int BASE_ID = 255 * 255 * 254; + static const unsigned int GRABBER_ELEMENTS_MAX_COUNT = 7; + + enum class EGrabberExtension + { + None = 0, + PosX = 1 << 0, + NegX = 1 << 1, + PosY = 1 << 2, + NegY = 1 << 3, + PosZ = 1 << 4, + NegZ = 1 << 5, + }; + + // Represents NO key(button on keyboard) value + static const int NO_SHORTCUT_KEY_VALUE = 0; + +protected: + struct Grabber + { + static const float SizeFactor; + static const float MinHalfSize; + static const float DraggingScaleFactor; + + bool enabled{ true }; + bool dragging{ false }; + Vec3d center{ Vec3d::Zero() }; + Vec3d angles{ Vec3d::Zero() }; +#if ENABLE_LEGACY_OPENGL_REMOVAL + Transform3d matrix{ Transform3d::Identity() }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + ColorRGBA color{ ColorRGBA::WHITE() }; + EGrabberExtension extensions{ EGrabberExtension::None }; +#if ENABLE_RAYCAST_PICKING + // the picking id shared by all the elements + int picking_id{ -1 }; + std::array, GRABBER_ELEMENTS_MAX_COUNT> raycasters = { nullptr }; +#endif // ENABLE_RAYCAST_PICKING + + Grabber() = default; + ~Grabber(); + +#if ENABLE_RAYCAST_PICKING + void render(bool hover, float size) { render(size, hover ? complementary(color) : color); } +#else + void render(bool hover, float size) { render(size, hover ? complementary(color) : color, false); } + void render_for_picking(float size) { render(size, color, true); } +#endif // ENABLE_RAYCAST_PICKING + + float get_half_size(float size) const; + float get_dragging_half_size(float size) const; + +#if ENABLE_RAYCAST_PICKING + void register_raycasters_for_picking(int id); + void unregister_raycasters_for_picking(); +#endif // ENABLE_RAYCAST_PICKING + + private: +#if ENABLE_RAYCAST_PICKING + void render(float size, const ColorRGBA& render_color); +#else + void render(float size, const ColorRGBA& render_color, bool picking); +#endif // ENABLE_RAYCAST_PICKING + +#if ENABLE_RAYCAST_PICKING + static PickingModel s_cube; + static PickingModel s_cone; +#else + static GLModel s_cube; + static GLModel s_cone; +#endif // ENABLE_RAYCAST_PICKING }; public: @@ -131,17 +134,17 @@ public: protected: GLCanvas3D& m_parent; - int m_group_id{ -1 }; // TODO: remove only for rotate - EState m_state{ Off }; - int m_shortcut_key{ 0 }; + int m_group_id; // TODO: remove only for rotate + EState m_state; + int m_shortcut_key; std::string m_icon_filename; unsigned int m_sprite_id; - int m_hover_id{ -1 }; - bool m_dragging{ false }; + int m_hover_id{ -1 }; + bool m_dragging{ false }; mutable std::vector m_grabbers; ImGuiWrapper* m_imgui; - bool m_first_input_window_render{ true }; - CommonGizmosDataPool* m_c{ nullptr }; + bool m_first_input_window_render{ true }; + CommonGizmosDataPool* m_c{ nullptr }; public: GLGizmoBase(GLCanvas3D& parent, @@ -183,9 +186,9 @@ public: bool update_items_state(); void render() { on_render(); } -#if !ENABLE_RAYCAST_PICKING +#if !ENABLE_RAYCAST_PICKING void render_for_picking() { on_render_for_picking(); } -#endif // !ENABLE_RAYCAST_PICKING +#endif // !ENABLE_RAYCAST_PICKING void render_input_window(float x, float y, float bottom_limit); /// @@ -207,13 +210,13 @@ public: /// Return True when use the information and don't want to propagate it otherwise False. virtual bool on_mouse(const wxMouseEvent &mouse_event) { return false; } -#if ENABLE_RAYCAST_PICKING - void register_raycasters_for_picking() { register_grabbers_for_picking(); on_register_raycasters_for_picking(); } - void unregister_raycasters_for_picking() { unregister_grabbers_for_picking(); on_unregister_raycasters_for_picking(); } +#if ENABLE_RAYCAST_PICKING + void register_raycasters_for_picking() { register_grabbers_for_picking(); on_register_raycasters_for_picking(); } + void unregister_raycasters_for_picking() { unregister_grabbers_for_picking(); on_unregister_raycasters_for_picking(); } #endif // ENABLE_RAYCAST_PICKING - virtual bool is_in_editing_mode() const { return false; } - virtual bool is_selection_rectangle_dragging() const { return false; } + virtual bool is_in_editing_mode() const { return false; } + virtual bool is_selection_rectangle_dragging() const { return false; } protected: virtual bool on_init() = 0; @@ -234,27 +237,27 @@ protected: virtual void on_dragging(const UpdateData& data) {} virtual void on_render() = 0; -#if !ENABLE_RAYCAST_PICKING +#if !ENABLE_RAYCAST_PICKING virtual void on_render_for_picking() = 0; -#endif // !ENABLE_RAYCAST_PICKING +#endif // !ENABLE_RAYCAST_PICKING virtual void on_render_input_window(float x, float y, float bottom_limit) {} -#if ENABLE_RAYCAST_PICKING - void register_grabbers_for_picking(); - void unregister_grabbers_for_picking(); - virtual void on_register_raycasters_for_picking() {} - virtual void on_unregister_raycasters_for_picking() {} -#else +#if ENABLE_RAYCAST_PICKING + void register_grabbers_for_picking(); + void unregister_grabbers_for_picking(); + virtual void on_register_raycasters_for_picking() {} + virtual void on_unregister_raycasters_for_picking() {} +#else // Returns the picking color for the given id, based on the BASE_ID constant // No check is made for clashing with other picking color (i.e. GLVolumes) ColorRGBA picking_color_component(unsigned int id) const; -#endif // ENABLE_RAYCAST_PICKING +#endif // ENABLE_RAYCAST_PICKING void render_grabbers(const BoundingBoxf3& box) const; void render_grabbers(float size) const; -#if !ENABLE_RAYCAST_PICKING +#if !ENABLE_RAYCAST_PICKING void render_grabbers_for_picking(const BoundingBoxf3& box) const; -#endif // !ENABLE_RAYCAST_PICKING +#endif // !ENABLE_RAYCAST_PICKING std::string format(float value, unsigned int decimals) const; @@ -277,7 +280,7 @@ protected: private: // Flag for dirty visible state of Gizmo // When True then need new rendering - bool m_dirty{ false }; + bool m_dirty{ false }; }; } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp new file mode 100644 index 0000000000..7a853e7187 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -0,0 +1,3438 @@ +#include "GLGizmoEmboss.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#include "slic3r/GUI/MainFrame.hpp" // to update title when add text +#include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/MsgDialog.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/CameraUtils.hpp" +#include "slic3r/GUI/Jobs/EmbossJob.hpp" +#include "slic3r/GUI/Jobs/CreateFontNameImageJob.hpp" +#include "slic3r/GUI/Jobs/NotificationProgressIndicator.hpp" +#include "slic3r/Utils/WxFontUtils.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +// TODO: remove include +#include "libslic3r/SVG.hpp" // debug store +#include "libslic3r/Geometry.hpp" // covex hull 2d +#include "libslic3r/Timer.hpp" // covex hull 2d + +#include "libslic3r/NSVGUtils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/ClipperUtils.hpp" // union_ex +#include "libslic3r/AppConfig.hpp" // store/load font list +#include "libslic3r/Format/OBJ.hpp" // load obj file for default object +#include "libslic3r/BuildVolume.hpp" + +#include "imgui/imgui_stdlib.h" // using std::string for inputs +#include "nanosvg/nanosvg.h" // load SVG file + +#include +#include +#include +#include + +#include + +#include +#include // measure enumeration of fonts + +// uncomment for easier debug +//#define ALLOW_DEBUG_MODE +#ifdef ALLOW_DEBUG_MODE +#define ALLOW_ADD_FONT_BY_FILE +#define ALLOW_ADD_FONT_BY_OS_SELECTOR +#define SHOW_WX_FONT_DESCRIPTOR // OS specific descriptor | file path --> in edit style +#define SHOW_FONT_FILE_PROPERTY // ascent, descent, line gap, cache --> in advanced +#define SHOW_FONT_COUNT // count of enumerated font --> in font combo box +#define SHOW_CONTAIN_3MF_FIX // when contain fix matrix --> show gray '3mf' next to close button +#define SHOW_OFFSET_DURING_DRAGGING // when drag with text over surface visualize used center +#define SHOW_IMGUI_ATLAS +#define SHOW_ICONS_TEXTURE +#define SHOW_FINE_POSITION // draw convex hull around volume +#define SHOW_WX_WEIGHT_INPUT +#define DRAW_PLACE_TO_ADD_TEXT +#endif // ALLOW_DEBUG_MODE + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; +using namespace Slic3r::GUI::Emboss; + +// anonymous namespace for unique names +namespace { +template +struct MinMax +{ + T min; + T max; +}; +template +struct Limit +{ + MinMax gui; + MinMax values; +}; +struct Limits +{ + MinMax emboss{0.01f, 1e4f}; + MinMax size_in_mm{0.1f, 1000.f}; + Limit boldness{{-200.f, 200.f}, {-2e4f, 2e4f}}; + Limit skew{{-1.f, 1.f}, {-100.f, 100.f}}; + MinMax char_gap{-20000, 20000}; + MinMax line_gap{-20000, 20000}; + // distance text object from surface + MinMax angle{-180.f, 180.f}; // in mm + + template + static bool apply(std::optional &val, const MinMax &limit) { + if (val.has_value()) + return apply(*val, limit); + return false; + } + template + static bool apply(T &val, const MinMax &limit) + { + if (val > limit.max) { + val = limit.max; + return true; + } + if (val < limit.min) { + val = limit.min; + return true; + } + return false; + } +}; +static const Limits limits; + +static bool is_text_empty(const std::string &text){ + return text.empty() || + text.find_first_not_of(" \n\t\r") == std::string::npos; +} + +// Normalize radian angle from -PI to PI +template void to_range_pi_pi(T& angle) +{ + if (angle > PI || angle < -PI) { + int count = static_cast(std::round(angle / (2 * PI))); + angle -= static_cast(count * 2 * PI); + } +} + +} // namespace + +GLGizmoEmboss::GLGizmoEmboss(GLCanvas3D &parent) + : GLGizmoBase(parent, M_ICON_FILENAME, -2) + , m_volume(nullptr) + , m_is_unknown_font(false) + , m_rotate_gizmo(parent, GLGizmoRotate::Axis::Z) // grab id = 2 (Z axis) + , m_style_manager(m_imgui->get_glyph_ranges()) + , m_update_job_cancel(nullptr) +{ + m_rotate_gizmo.set_group_id(0); + m_rotate_gizmo.set_using_local_coordinate(true); + // TODO: add suggestion to use https://fontawesome.com/ + // (copy & paste) unicode symbols from web + // paste HEX unicode into notepad move cursor after unicode press [alt] + [x] +} + +// Private namespace with helper function for create volume +namespace priv { + +/// +/// Prepare data for emboss +/// +/// Text to emboss +/// Keep actual selected style +/// Base data for emboss text +static DataBase create_emboss_data_base(const std::string &text, StyleManager &style_manager); + +static bool is_valid(ModelVolumeType volume_type); + +/// +/// Start job for add new volume to object with given transformation +/// +/// Define where to add +/// Volume transformation +/// Define text +/// Type of volume +static void start_create_volume_job(const ModelObject *object, + const Transform3d volume_trmat, + DataBase &emboss_data, + ModelVolumeType volume_type); + +static GLVolume *get_hovered_gl_volume(const GLCanvas3D &canvas); + +/// +/// Start job for add new volume on surface of object defined by screen coor +/// +/// Define params of text +/// Emboss / engrave +/// Mouse position which define position +/// Volume to find surface for create +/// Ability to ray cast to model +/// True when start creation, False when there is no hit surface by screen coor +static bool start_create_volume_on_surface_job(DataBase &emboss_data, + ModelVolumeType volume_type, + const Vec2d &screen_coor, + const GLVolume *gl_volume, + RaycastManager &raycaster); + +/// +/// Find volume in selected object with closest convex hull to screen center. +/// Return +/// +/// Define where to search for closest +/// Canvas center(dependent on camera settings) +/// Actual objects +/// OUT: coordinate of controid of closest volume +/// OUT: closest volume +static void find_closest_volume(const Selection &selection, + const Vec2d &screen_center, + const Camera &camera, + const ModelObjectPtrs &objects, + Vec2d *closest_center, + const GLVolume **closest_volume); + +/// +/// Start job for add object with text into scene +/// +/// Define params of text +/// Screen coordinat, where to create new object laying on bed +static void start_create_object_job(DataBase &emboss_data, const Vec2d &coor); + +static void message_disable_cut_surface(){ + wxMessageBox(_L("Can NOT cut surface from nothing. Function 'use surface' was disabled for this text."), + _L("Disable 'use surface' from style"), wxOK | wxICON_WARNING);} + +/// +/// Create transformation for new created emboss object by mouse position +/// +/// Define where to add object +/// Actual camera view +/// Define shape of bed for its center and check that coor is on bed center +/// Emboss size / 2 +/// Transformation for create text on bed +static Transform3d create_transformation_on_bed(const Vec2d &screen_coor, + const Camera &camera, + const std::vector &bed_shape, + double z); +} // namespace priv + +bool priv::is_valid(ModelVolumeType volume_type){ + if (volume_type == ModelVolumeType::MODEL_PART || + volume_type == ModelVolumeType::NEGATIVE_VOLUME || + volume_type == ModelVolumeType::PARAMETER_MODIFIER) + return true; + + BOOST_LOG_TRIVIAL(error) << "Can't create embossed volume with this type: " << (int)volume_type; + return false; +} + +void GLGizmoEmboss::create_volume(ModelVolumeType volume_type, const Vec2d& mouse_pos) +{ + if (!priv::is_valid(volume_type)) return; + if (!m_gui_cfg.has_value()) initialize(); + set_default_text(); + m_style_manager.discard_style_changes(); + + GLVolume *gl_volume = priv::get_hovered_gl_volume(m_parent); + DataBase emboss_data = priv::create_emboss_data_base(m_text, m_style_manager); + // Try to cast ray into scene and find object for add volume + if (priv::start_create_volume_on_surface_job(emboss_data, volume_type, mouse_pos, gl_volume, m_raycast_manager)) + // object found + return; + + // object is not under mouse position soo create object on plater + priv::start_create_object_job(emboss_data, mouse_pos); +} + +void GLGizmoEmboss::create_volume(ModelVolumeType volume_type) +{ + if (!priv::is_valid(volume_type)) return; + if (!m_gui_cfg.has_value()) initialize(); + set_default_text(); + m_style_manager.discard_style_changes(); + + // select position by camera position and view direction + const Selection &selection = m_parent.get_selection(); + int object_idx = selection.get_object_idx(); + + Size s = m_parent.get_canvas_size(); + Vec2d screen_center(s.get_width() / 2., s.get_height() / 2.); + DataBase emboss_data = priv::create_emboss_data_base(m_text, m_style_manager); + if (!selection.is_empty() && object_idx >= 0) { + // create volume inside of object + const Plater &plater = *wxGetApp().plater(); + const Camera &camera = plater.get_camera(); + const ModelObjectPtrs &objects = wxGetApp().model().objects; + + Vec2d coor; + const GLVolume *vol = nullptr; + priv::find_closest_volume(selection, screen_center, camera, objects, &coor, &vol); + if (!priv::start_create_volume_on_surface_job(emboss_data, volume_type, coor, vol, m_raycast_manager)) { + assert(vol != nullptr); + // in centroid of convex hull is not hit with object + // soo create transfomation on border of object + const ModelObject *obj = objects[vol->object_idx()]; + const BoundingBoxf3& bb = obj->bounding_box(); + Transform3d volume_trmat(Eigen::Translation3d(bb.max.x(), 0., 0.)); + priv::start_create_volume_job(obj, volume_trmat, emboss_data, volume_type); + } + } else { + // create Object on center of screen + // when ray throw center of screen not hit bed it create object on center of bed + priv::start_create_object_job(emboss_data, screen_center); + } +} + +bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) +{ + if (mouse_event.Moving()) return false; + + bool used = use_grabbers(mouse_event); + if (!m_dragging) return used; + + if (mouse_event.Dragging()) { + auto &angle_opt = m_volume->text_configuration->style.prop.angle; + if (!m_rotate_start_angle.has_value()) + m_rotate_start_angle = angle_opt.has_value() ? *angle_opt : 0.f; + double angle = m_rotate_gizmo.get_angle(); + angle -= PI / 2; // Grabber is upward + + // temporary rotation + TransformationType transformation_type = TransformationType::Local_Relative_Joint; + m_parent.get_selection().rotate(Vec3d(0., 0., angle), transformation_type); + + angle += *m_rotate_start_angle; + // move to range <-M_PI, M_PI> + to_range_pi_pi(angle); + // propagate angle into property + angle_opt = static_cast(angle); + + // do not store zero + if (is_approx(*angle_opt, 0.f)) + angle_opt.reset(); + + // set into activ style + assert(m_style_manager.is_activ_font()); + if (m_style_manager.is_activ_font()) + m_style_manager.get_font_prop().angle = angle_opt; + + } + return used; +} + +namespace priv { + +/// +/// Access to model from gl_volume +/// TODO: it is more general function --> move to utils +/// +/// Volume to model belongs to +/// All objects +/// Model for volume +static ModelVolume *get_model_volume(const GLVolume *gl_volume, const ModelObjectPtrs &objects); + +/// +/// Access to model by selection +/// TODO: it is more general function --> move to select utils +/// +/// Actual selection +/// All objects +/// Model from selection +static ModelVolume *get_selected_volume(const Selection &selection, const ModelObjectPtrs &objects); + +/// +/// Calculate offset from mouse position to center of text +/// +/// Screan mouse position +/// Selected volume(text) +/// Offset in screan coordinate +static Vec2d calc_mouse_to_center_text_offset(const Vec2d &mouse, const ModelVolume &mv); + +} // namespace priv + +Vec2d priv::calc_mouse_to_center_text_offset(const Vec2d& mouse, const ModelVolume& mv) { + const Transform3d &volume_tr = mv.get_matrix(); + const Camera &camera = wxGetApp().plater()->get_camera(); + assert(mv.text_configuration.has_value()); + + auto calc_offset = [&mouse, &volume_tr, &camera, &mv] + (const Transform3d &instrance_tr) -> Vec2d { + Transform3d to_world = instrance_tr * volume_tr; + + // Use fix of .3mf loaded tranformation when exist + if (mv.text_configuration->fix_3mf_tr.has_value()) + to_world = to_world * (*mv.text_configuration->fix_3mf_tr); + // zero point of volume in world coordinate system + Vec3d volume_center = to_world.translation(); + // screen coordinate of volume center + Vec2i coor = CameraUtils::project(camera, volume_center); + return coor.cast() - mouse; + }; + + auto object = mv.get_object(); + assert(!object->instances.empty()); + // Speed up for one instance + if (object->instances.size() == 1) + return calc_offset(object->instances.front()->get_matrix()); + + Vec2d nearest_offset; + double nearest_offset_size = std::numeric_limits::max(); + for (const ModelInstance *instance : object->instances) { + Vec2d offset = calc_offset(instance->get_matrix()); + double offset_size = offset.norm(); + if (nearest_offset_size < offset_size) continue; + nearest_offset_size = offset_size; + nearest_offset = offset; + } + return nearest_offset; +} + +bool GLGizmoEmboss::on_mouse_for_translate(const wxMouseEvent &mouse_event) +{ + // filter events + if (!(mouse_event.Dragging() && mouse_event.LeftIsDown()) && + !mouse_event.LeftUp() && + !mouse_event.LeftDown()) + return false; + + // must exist hover object + int hovered_id = m_parent.get_first_hover_volume_idx(); + if (hovered_id < 0) return false; + + GLVolume *gl_volume = m_parent.get_volumes().volumes[hovered_id]; + const ModelObjectPtrs &objects = wxGetApp().plater()->model().objects; + ModelVolume *act_model_volume = priv::get_model_volume(gl_volume, objects); + + // hovered object must be actual text volume + if (m_volume != act_model_volume) return false; + + const ModelVolumePtrs &volumes = m_volume->get_object()->volumes; + std::vector allowed_volumes_id; + if (volumes.size() > 1) { + allowed_volumes_id.reserve(volumes.size() - 1); + for (auto &v : volumes) { + if (v->id() == m_volume->id()) continue; + if (!v->is_model_part()) continue; + allowed_volumes_id.emplace_back(v->id().id); + } + } + + // wxCoord == int --> wx/types.h + Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); + Vec2d mouse_pos = mouse_coord.cast(); + RaycastManager::AllowVolumes condition(std::move(allowed_volumes_id)); + + // detect start text dragging + if (mouse_event.LeftDown()) { + // initialize raycasters + // IMPROVE: move to job, for big scene it slows down + ModelObject *act_model_object = act_model_volume->get_object(); + m_raycast_manager.actualize(act_model_object, &condition); + m_dragging_mouse_offset = priv::calc_mouse_to_center_text_offset(mouse_pos, *m_volume); + // Cancel job to prevent interuption of dragging (duplicit result) + if (m_update_job_cancel != nullptr) m_update_job_cancel->store(true); + return false; + } + + // Dragging starts out of window + if (!m_dragging_mouse_offset.has_value()) return false; + + const Camera &camera = wxGetApp().plater()->get_camera(); + Vec2d offseted_mouse = mouse_pos + *m_dragging_mouse_offset; + auto hit = m_raycast_manager.unproject(offseted_mouse, camera, &condition); + if (!hit.has_value()) { + // there is no hit + // show common translation of object + m_parent.toggle_model_objects_visibility(true); + m_temp_transformation = {}; + return false; + } + + if (mouse_event.Dragging()) { + TextConfiguration &tc = *m_volume->text_configuration; + // hide common dragging of object + m_parent.toggle_model_objects_visibility(false, m_volume->get_object(), gl_volume->instance_idx(), m_volume); + + // Calculate temporary position + Transform3d object_trmat = m_raycast_manager.get_transformation(hit->tr_key); + Transform3d trmat = create_transformation_onto_surface(hit->position, hit->normal); + const FontProp& font_prop = tc.style.prop; + apply_transformation(font_prop, trmat); + + // fix baked transformation from .3mf store process + if (tc.fix_3mf_tr.has_value()) + trmat = trmat * (*tc.fix_3mf_tr); + + // temp is in wolrld coors + m_temp_transformation = object_trmat * trmat; + } else if (mouse_event.LeftUp()) { + // Added because of weird case after double click into scene + // with Mesa driver OR on Linux + if (!m_temp_transformation.has_value()) return false; + + // TODO: Disable applying of common transformation after draggig + // Call after is used for apply transformation after common dragging to rewrite it + Transform3d volume_trmat = + gl_volume->get_instance_transformation().get_matrix().inverse() * + *m_temp_transformation; + wxGetApp().plater()->CallAfter([volume_trmat, mv = m_volume]() { + mv->set_transformation(volume_trmat); + }); + + m_parent.toggle_model_objects_visibility(true); + // Apply temporary position + m_temp_transformation = {}; + m_dragging_mouse_offset = {}; + + // Update surface by new position + if (m_volume->text_configuration->style.prop.use_surface) { + // need actual position + m_volume->set_transformation(volume_trmat); + process(); + } + } + return false; +} + +bool GLGizmoEmboss::on_mouse(const wxMouseEvent &mouse_event) +{ + // do not process moving event + if (mouse_event.Moving()) return false; + + // not selected volume + assert(m_volume != nullptr); + assert(m_volume->text_configuration.has_value()); + if (m_volume == nullptr || !m_volume->text_configuration.has_value()) return false; + + if (on_mouse_for_rotation(mouse_event)) return true; + if (on_mouse_for_translate(mouse_event)) return true; + + return false; +} + +bool GLGizmoEmboss::on_init() +{ + m_rotate_gizmo.init(); + ColorRGBA gray_color(.6f, .6f, .6f, .3f); + m_rotate_gizmo.set_highlight_color(gray_color); + return true; +} + +std::string GLGizmoEmboss::on_get_name() const { return _u8L("Emboss"); } + +void GLGizmoEmboss::on_render() { + // no volume selected + if (m_volume == nullptr) return; + Selection &selection = m_parent.get_selection(); + if (selection.is_empty()) return; + + if (m_temp_transformation.has_value()) { + // draw text volume on temporary position +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLVolume& gl_volume = *selection.get_volume(*selection.get_volume_idxs().begin()); +#else + const GLVolume& gl_volume = *selection.get_volume(*selection.get_volume_idxs().begin()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); +#else + glsafe(::glPushMatrix()); + glsafe(::glMultMatrixd(m_temp_transformation->data())); + GLShaderProgram *shader = wxGetApp().get_shader("gouraud_light"); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + shader->start_using(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d matrix = camera.get_view_matrix() * (*m_temp_transformation); + shader->set_uniform("view_model_matrix", matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + shader->set_uniform("view_normal_matrix", (Matrix3d) (matrix).matrix().block(0, 0, 3, 3).inverse().transpose()); + shader->set_uniform("emission_factor", 0.0f); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // dragging object must be selected so draw it with correct color + //auto color = gl_volume.color; + //auto color = gl_volume.render_color; + auto color = GLVolume::SELECTED_COLOR; + // Set transparent color for NEGATIVE_VOLUME & PARAMETER_MODIFIER + bool is_transparent = m_volume->type() != ModelVolumeType::MODEL_PART; + if (is_transparent) { + color.a(0.5f); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + } +#if !ENABLE_LEGACY_OPENGL_REMOVAL + shader->set_uniform("uniform_color", color); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_DEPTH_TEST)); +#if ENABLE_LEGACY_OPENGL_REMOVAL + gl_volume.model.set_color(color); + gl_volume.model.render(); +#else + gl_volume.indexed_vertex_array.render(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glDisable(GL_DEPTH_TEST)); + + if (is_transparent) glsafe(::glDisable(GL_BLEND)); + + shader->stop_using(); +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + } + + bool is_surface_dragging = m_temp_transformation.has_value(); + // Do NOT render rotation grabbers when dragging object + bool is_rotate_by_grabbers = m_dragging; + if (!is_surface_dragging && + (!m_parent.is_dragging() || is_rotate_by_grabbers)) { + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + m_rotate_gizmo.render(); + } +} + +#if ENABLE_RAYCAST_PICKING +void GLGizmoEmboss::on_register_raycasters_for_picking(){ + m_rotate_gizmo.register_raycasters_for_picking(); +} +void GLGizmoEmboss::on_unregister_raycasters_for_picking(){ + m_rotate_gizmo.unregister_raycasters_for_picking(); +} +#else // !ENABLE_RAYCAST_PICKING +void GLGizmoEmboss::on_render_for_picking() { + m_rotate_gizmo.render_for_picking(); +} +#endif // ENABLE_RAYCAST_PICKING + +#ifdef SHOW_FINE_POSITION +// draw suggested position of window +static void draw_fine_position(const Selection &selection, + const Size &canvas, + const ImVec2 &windows_size) +{ + const Selection::IndicesList& indices = selection.get_volume_idxs(); + // no selected volume + if (indices.empty()) return; + const GLVolume *volume = selection.get_volume(*indices.begin()); + // bad volume selected (e.g. deleted one) + if (volume == nullptr) return; + + const Camera &camera = wxGetApp().plater()->get_camera(); + Slic3r::Polygon hull = CameraUtils::create_hull2d(camera, *volume); + ImVec2 canvas_size(canvas.get_width(), canvas.get_height()); + ImVec2 offset = ImGuiWrapper::suggest_location(windows_size, hull, + canvas_size); + Slic3r::Polygon rect( + {Point(offset.x, offset.y), Point(offset.x + windows_size.x, offset.y), + Point(offset.x + windows_size.x, offset.y + windows_size.y), + Point(offset.x, offset.y + windows_size.y)}); + ImGuiWrapper::draw(hull); + ImGuiWrapper::draw(rect); +} +#endif // SHOW_FINE_POSITION + +#ifdef DRAW_PLACE_TO_ADD_TEXT +static void draw_place_to_add_text() +{ + ImVec2 mp = ImGui::GetMousePos(); + Vec2d mouse_pos(mp.x, mp.y); + const Camera &camera = wxGetApp().plater()->get_camera(); + Vec3d p1 = CameraUtils::get_z0_position(camera, mouse_pos); + std::vector rect3d{p1 + Vec3d(5, 5, 0), p1 + Vec3d(-5, 5, 0), + p1 + Vec3d(-5, -5, 0), p1 + Vec3d(5, -5, 0)}; + Points rect2d = CameraUtils::project(camera, rect3d); + ImGuiWrapper::draw(Slic3r::Polygon(rect2d)); +} +#endif // DRAW_PLACE_TO_ADD_TEXT + +#ifdef SHOW_OFFSET_DURING_DRAGGING +static void draw_mouse_offset(const std::optional &offset) +{ + if (!offset.has_value()) return; + // debug draw + auto draw_list = ImGui::GetOverlayDrawList(); + ImVec2 p1 = ImGui::GetMousePos(); + ImVec2 p2(p1.x + offset->x(), p1.y + offset->y()); + ImU32 color = ImGui::GetColorU32(ImGuiWrapper::COL_ORANGE_LIGHT); + float thickness = 3.f; + draw_list->AddLine(p1, p2, color, thickness); +} +#endif // SHOW_OFFSET_DURING_DRAGGING + +void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit) +{ + if (!m_gui_cfg.has_value()) initialize(); + check_selection(); + + // Do not render window for not selected text volume + if (m_volume == nullptr || !m_volume->text_configuration.has_value()) { + close(); + return; + } + + // TODO: fix width - showing scroll in first draw of advanced. + const ImVec2 &min_window_size = get_minimal_window_size(); + ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, min_window_size); + +#ifdef SHOW_FINE_POSITION + draw_fine_position(m_parent.get_selection(), m_parent.get_canvas_size(), min_window_size); +#endif // SHOW_FINE_POSITION +#ifdef DRAW_PLACE_TO_ADD_TEXT + draw_place_to_add_text(); +#endif // DRAW_PLACE_TO_ADD_TEXT +#ifdef SHOW_OFFSET_DURING_DRAGGING + draw_mouse_offset(m_dragging_mouse_offset); +#endif // SHOW_OFFSET_DURING_DRAGGING + + // check if is set window offset + if (m_set_window_offset.has_value()) { + ImGui::SetNextWindowPos(*m_set_window_offset, ImGuiCond_Always); + m_set_window_offset.reset(); + } + + ImGuiWindowFlags flag = ImGuiWindowFlags_NoCollapse; + if (ImGui::Begin(on_get_name().c_str(), nullptr, flag)) { + // Need to pop var before draw window + ImGui::PopStyleVar(); // WindowMinSize + draw_window(); + } else { + ImGui::PopStyleVar(); // WindowMinSize + } + ImGui::End(); +} + +void GLGizmoEmboss::on_set_state() +{ + // enable / disable bed from picking + // Rotation gizmo must work through bed + m_parent.set_raycaster_gizmos_on_top(GLGizmoBase::m_state == GLGizmoBase::On); + + m_rotate_gizmo.set_state(GLGizmoBase::m_state); + + // Closing gizmo. e.g. selecting another one + if (GLGizmoBase::m_state == GLGizmoBase::Off) { + // refuse outgoing during text preview + if (false) { + GLGizmoBase::m_state = GLGizmoBase::On; + auto notification_manager = wxGetApp().plater()->get_notification_manager(); + notification_manager->push_notification( + NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + _u8L("ERROR: Wait until ends or Cancel process.")); + return; + } + m_volume = nullptr; + // Store order and last activ index into app.ini + // TODO: what to do when can't store into file? + m_style_manager.store_styles_to_app_config(false); + remove_notification_not_valid_font(); + } else if (GLGizmoBase::m_state == GLGizmoBase::On) { + if (!m_gui_cfg.has_value()) initialize(); + + // to reload fonts from system, when install new one + wxFontEnumerator::InvalidateCache(); + + // Try(when exist) set text configuration by volume + load_configuration(get_selected_volume()); + + // change position of just opened emboss window + set_fine_position(); + + // when open by hyperlink it needs to show up + // or after key 'T' windows doesn't appear + m_parent.set_as_dirty(); + } +} + +void GLGizmoEmboss::on_start_dragging() { m_rotate_gizmo.start_dragging(); } +void GLGizmoEmboss::on_stop_dragging() +{ + m_rotate_gizmo.stop_dragging(); + + // TODO: when start second rotatiton previous rotation rotate draggers + // This is fast fix for second try to rotate + // When fixing, move grabber above text (not on side) + m_rotate_gizmo.set_angle(PI/2); + + // apply rotation + m_parent.do_rotate(L("Text-Rotate")); + + m_rotate_start_angle.reset(); + + // recalculate for surface cut + const FontProp &font_prop = m_style_manager.get_style().prop; + if (font_prop.use_surface) process(); +} +void GLGizmoEmboss::on_dragging(const UpdateData &data) { m_rotate_gizmo.dragging(data); } + +void GLGizmoEmboss::initialize() +{ + if (m_gui_cfg.has_value()) return; + + GuiCfg cfg; // initialize by default values; + float line_height = ImGui::GetTextLineHeight(); + float line_height_with_spacing = ImGui::GetTextLineHeightWithSpacing(); + float space = line_height_with_spacing - line_height; + + cfg.max_style_name_width = ImGui::CalcTextSize("Maximal font name, extended").x; + + cfg.icon_width = std::ceil(line_height); + // make size pair number + if (cfg.icon_width % 2 != 0) ++cfg.icon_width; + + cfg.delete_pos_x = cfg.max_style_name_width + space; + int count_line_of_text = 3; + cfg.text_size = ImVec2(-FLT_MIN, line_height_with_spacing * count_line_of_text); + ImVec2 letter_m_size = ImGui::CalcTextSize("M"); + int count_letter_M_in_input = 12; + cfg.input_width = letter_m_size.x * count_letter_M_in_input; + GuiCfg::Translations &tr = cfg.translations; + tr.type = _u8L("Type"); + tr.style = _u8L("Style"); + float max_style_text_width = std::max( + ImGui::CalcTextSize(tr.type.c_str()).x, + ImGui::CalcTextSize(tr.style.c_str()).x); + cfg.style_offset = max_style_text_width + 3 * space; + + tr.font = _u8L("Font"); + tr.size = _u8L("Height"); + tr.depth = _u8L("Depth"); + float max_text_width = std::max({ + ImGui::CalcTextSize(tr.font.c_str()).x, + ImGui::CalcTextSize(tr.size.c_str()).x, + ImGui::CalcTextSize(tr.depth.c_str()).x}); + cfg.input_offset = max_text_width + + 3 * space + ImGui::GetTreeNodeToLabelSpacing(); + + tr.use_surface = _u8L("Use surface"); + tr.char_gap = _u8L("Char gap"); + tr.line_gap = _u8L("Line gap"); + tr.boldness = _u8L("Boldness"); + tr.italic = _u8L("Skew ratio"); + tr.surface_distance = _u8L("Z-move"); + tr.angle = _u8L("Z-rot"); + tr.collection = _u8L("Collection"); + float max_advanced_text_width = std::max({ + ImGui::CalcTextSize(tr.use_surface.c_str()).x, + ImGui::CalcTextSize(tr.char_gap.c_str()).x, + ImGui::CalcTextSize(tr.line_gap.c_str()).x, + ImGui::CalcTextSize(tr.boldness.c_str()).x, + ImGui::CalcTextSize(tr.italic.c_str()).x, + ImGui::CalcTextSize(tr.surface_distance.c_str()).x, + ImGui::CalcTextSize(tr.angle.c_str()).x, + ImGui::CalcTextSize(tr.collection.c_str()).x }); + cfg.advanced_input_offset = max_advanced_text_width + + 3 * space + ImGui::GetTreeNodeToLabelSpacing(); + + // calculate window size + const ImGuiStyle &style = ImGui::GetStyle(); + float window_title = line_height + 2*style.FramePadding.y; + float input_height = line_height_with_spacing + 2*style.FramePadding.y; + float tree_header = line_height_with_spacing; + float window_height = + window_title + // window title + cfg.text_size.y + // text field + input_height * 6 + // type Radios + style selector + font name + + // height + depth + close button + tree_header + // advance tree + 2 * style.WindowPadding.y; + float window_width = cfg.style_offset + cfg.input_width + 2*style.WindowPadding.x + + 4 * (cfg.icon_width + space); + cfg.minimal_window_size = ImVec2(window_width, window_height); + + // 6 = charGap, LineGap, Bold, italic, surfDist, angle + // 4 = 1px for fix each edit image of drag float + float advance_height = input_height * 7 + 8; + cfg.minimal_window_size_with_advance = + ImVec2(cfg.minimal_window_size.x, + cfg.minimal_window_size.y + advance_height); + + cfg.minimal_window_size_with_collections = + ImVec2(cfg.minimal_window_size_with_advance.x, + cfg.minimal_window_size_with_advance.y + input_height); + + int max_style_image_width = cfg.max_style_name_width /2 - + 2 * style.FramePadding.x; + int max_style_image_height = 1.5 * input_height; + cfg.max_style_image_size = Vec2i(max_style_image_width, max_style_image_height); + cfg.face_name_size.y() = line_height_with_spacing; + + m_gui_cfg.emplace(std::move(cfg)); + + init_icons(); + + // initialize text styles + m_style_manager.init(wxGetApp().app_config, create_default_styles()); + set_default_text(); + + // Set rotation gizmo upwardrotate + m_rotate_gizmo.set_angle(PI/2); +} + +EmbossStyles GLGizmoEmboss::create_default_styles() +{ + // https://docs.wxwidgets.org/3.0/classwx_font.html + // Predefined objects/pointers: wxNullFont, wxNORMAL_FONT, wxSMALL_FONT, wxITALIC_FONT, wxSWISS_FONT + return { + WxFontUtils::create_emboss_style(*wxNORMAL_FONT, _u8L("NORMAL")), // wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT) + WxFontUtils::create_emboss_style(*wxSMALL_FONT, _u8L("SMALL")), // A font using the wxFONTFAMILY_SWISS family and 2 points smaller than wxNORMAL_FONT. + WxFontUtils::create_emboss_style(*wxITALIC_FONT, _u8L("ITALIC")), // A font using the wxFONTFAMILY_ROMAN family and wxFONTSTYLE_ITALIC style and of the same size of wxNORMAL_FONT. + WxFontUtils::create_emboss_style(*wxSWISS_FONT, _u8L("SWISS")), // A font identic to wxNORMAL_FONT except for the family used which is wxFONTFAMILY_SWISS. + WxFontUtils::create_emboss_style(wxFont(10, wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD), _u8L("MODERN")) + }; +} + +// Could exist systems without installed font so last chance is used own file +//EmbossStyle GLGizmoEmboss::create_default_style() { +// std::string font_path = Slic3r::resources_dir() + "/fonts/NotoSans-Regular.ttf"; +// return EmbossStyle{"Default font", font_path, EmbossStyle::Type::file_path}; +//} + +void GLGizmoEmboss::set_default_text(){ m_text = _u8L("Embossed text"); } + +#include "imgui/imgui_internal.h" // to unfocus input --> ClearActiveID +void GLGizmoEmboss::check_selection() +{ + ModelVolume *vol = get_selected_volume(); + // is same volume selected? + if (vol != nullptr && m_volume == vol) return; + + // for changed volume notification is NOT valid + remove_notification_not_valid_font(); + + // Do not use focused input value when switch volume(it must swith value) + if (m_volume != nullptr) ImGui::ClearActiveID(); + + // is select embossed volume? + if (load_configuration(vol)) + // successfull load volume for editing + return; + + // behave like adding new text + m_volume = nullptr; + set_default_text(); +} + +ModelVolume *priv::get_model_volume(const GLVolume *gl_volume, const ModelObjectPtrs &objects) +{ + const GLVolume::CompositeID &id = gl_volume->composite_id; + + if (id.object_id < 0 || static_cast(id.object_id) >= objects.size()) return nullptr; + ModelObject *object = objects[id.object_id]; + + if (id.volume_id < 0 || static_cast(id.volume_id) >= object->volumes.size()) return nullptr; + return object->volumes[id.volume_id]; +} + +ModelVolume *priv::get_selected_volume(const Selection &selection, const ModelObjectPtrs &objects) +{ + int object_idx = selection.get_object_idx(); + // is more object selected? + if (object_idx == -1) return nullptr; + + auto volume_idxs = selection.get_volume_idxs(); + // is more volumes selected? + if (volume_idxs.size() != 1) return nullptr; + unsigned int vol_id_gl = *volume_idxs.begin(); + const GLVolume *vol_gl = selection.get_volume(vol_id_gl); + return get_model_volume(vol_gl, objects); +} + +ModelVolume *GLGizmoEmboss::get_selected_volume() +{ + return priv::get_selected_volume(m_parent.get_selection(), + wxGetApp().plater()->model().objects); +} + +// Run Job on main thread (blocking) - ONLY DEBUG +static inline void execute_job(std::shared_ptr j) +{ + struct MyCtl : public Job::Ctl + { + void update_status(int st, const std::string &msg = "") override{}; + bool was_canceled() const override { return false; } + std::future call_on_main_thread(std::function fn) override + { + return std::future{}; + } + } ctl; + j->process(ctl); + wxGetApp().plater()->CallAfter([j]() { + std::exception_ptr e_ptr = nullptr; + j->finalize(false, e_ptr); + }); +} + +bool GLGizmoEmboss::process() +{ + // no volume is selected -> selection from right panel + assert(m_volume != nullptr); + if (m_volume == nullptr) return false; + + // without text there is nothing to emboss + if (m_text.empty()) return false; + + // exist loaded font file? + if (!m_style_manager.is_activ_font()) return false; + + // Cancel previous Job, when it is in process + // Can't use cancel, because I want cancel only previous EmbossUpdateJob no other jobs + // worker.cancel(); + // Cancel only EmbossUpdateJob no others + if (m_update_job_cancel != nullptr) + m_update_job_cancel->store(true); + // create new shared ptr to cancel new job + m_update_job_cancel = std::make_shared >(false); + DataUpdate data{priv::create_emboss_data_base(m_text, m_style_manager), m_volume->id(), m_update_job_cancel}; + + std::unique_ptr job = nullptr; + + // check cutting from source mesh + bool &use_surface = data.text_configuration.style.prop.use_surface; + bool is_object = m_volume->get_object()->volumes.size() == 1; + if (use_surface && is_object) { + priv::message_disable_cut_surface(); + use_surface = false; + } + + + if (use_surface) { + // Model to cut surface from. + SurfaceVolumeData::ModelSources sources = create_volume_sources(m_volume); + if (sources.empty()) return false; + + Transform3d text_tr = m_volume->get_matrix(); + auto& fix_3mf = m_volume->text_configuration->fix_3mf_tr; + if (fix_3mf.has_value()) + text_tr = text_tr * fix_3mf->inverse(); + + bool is_outside = m_volume->is_model_part(); + // check that there is not unexpected volume type + assert(is_outside || m_volume->is_negative_volume() || + m_volume->is_modifier()); + UpdateSurfaceVolumeData surface_data{std::move(data), text_tr, is_outside, std::move(sources)}; + job = std::make_unique(std::move(surface_data)); + } else { + job = std::make_unique(std::move(data)); + } + + //* + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::move(job)); + /*/ // Run Job on main thread (blocking) - ONLY DEBUG + execute_job(std::move(job)); + // */ + + // notification is removed befor object is changed by job + remove_notification_not_valid_font(); + return true; +} + +void GLGizmoEmboss::close() +{ + // remove volume when text is empty + if (m_volume != nullptr && + m_volume->text_configuration.has_value() && + is_text_empty(m_text)) { + Plater &p = *wxGetApp().plater(); + if (is_text_object(m_volume)) { + // delete whole object + p.remove(m_parent.get_selection().get_object_idx()); + } else { + // delete text volume + p.remove_selected(); + } + } + + // prepare for new opening + m_unmodified_volume.reset(); + + // close gizmo == open it again + auto& mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() == GLGizmosManager::Emboss) + mng.open_gizmo(GLGizmosManager::Emboss); +} + +void GLGizmoEmboss::discard_and_close() { + if (!m_unmodified_volume.has_value()) return; + m_volume->set_transformation(m_unmodified_volume->tr); + UpdateJob::update_volume(m_volume, std::move(m_unmodified_volume->tm), m_unmodified_volume->tc, m_unmodified_volume->name); + close(); + + //auto plater = wxGetApp().plater(); + // 2 .. on set state off, history is squashed into 'emboss_begin' and 'emboss_end' + //plater->undo_to(2); // undo before open emboss gizmo + // TODO: need fix after move to find correct undo timestamp or different approach + // It is weird ford user that after discard changes it is moving with history + + // NOTE: Option to remember state before edit: + // * Need triangle mesh(memory consuming), volume name, transformation + TextConfiguration + // * Can't revert volume id. + // * Need to refresh a lot of stored data. More info in implementation EmbossJob.cpp -> update_volume() + // * Volume containing 3mf fix transformation - needs work around +} + +void GLGizmoEmboss::draw_window() +{ +#ifdef ALLOW_DEBUG_MODE + if (ImGui::Button("re-process")) process(); + if (ImGui::Button("add svg")) choose_svg_file(); +#endif // ALLOW_DEBUG_MODE + + bool is_activ_font = m_style_manager.is_activ_font(); + if (!is_activ_font) + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Warning: No font is selected. Select correct one.")); + + // Disable all except selection of font, when open text from 3mf with unknown font + m_imgui->disabled_begin(m_is_unknown_font); + ScopeGuard unknown_font_sc([&]() { + m_imgui->disabled_end(); + }); + + draw_text_input(); + draw_model_type(); + draw_style_list(); + m_imgui->disabled_begin(!is_activ_font); + ImGui::TreePush(); + draw_style_edit(); + ImGui::TreePop(); + + // close advanced style property when unknown font is selected + if (m_is_unknown_font && m_is_advanced_edit_style) + ImGui::SetNextTreeNodeOpen(false); + + if (ImGui::TreeNode(_u8L("advanced").c_str())) { + if (!m_is_advanced_edit_style) { + set_minimal_window_size(true); + } else { + draw_advanced(); + } + ImGui::TreePop(); + } else if (m_is_advanced_edit_style) + set_minimal_window_size(false); + m_imgui->disabled_end(); // !is_activ_font + +#ifdef SHOW_WX_FONT_DESCRIPTOR + if (is_selected_style) + m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, m_style_manager.get_style().path); +#endif // SHOW_WX_FONT_DESCRIPTOR + + ImGui::PushStyleColor(ImGuiCol_Button, ImGuiWrapper::COL_GREY_DARK); + if (ImGui::Button(_u8L("Close").c_str())) + discard_and_close(); + else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Discard changes on embossed text and close.").c_str()); + ImGui::PopStyleColor(); + + ImGui::SameLine(); + if (ImGui::Button(_u8L("Apply").c_str())) { + if (m_is_unknown_font) { + process(); + } else { + close(); + } + } + +#ifdef SHOW_CONTAIN_3MF_FIX + if (m_volume!=nullptr && + m_volume->text_configuration.has_value() && + m_volume->text_configuration->fix_3mf_tr.has_value()) { + ImGui::SameLine(); + m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, ".3mf"); + if (ImGui::IsItemHovered()) { + Transform3d &fix = *m_volume->text_configuration->fix_3mf_tr; + std::stringstream ss; + ss << fix.matrix(); + std::string filename = (m_volume->source.input_file.empty())? "unknown.3mf" : + m_volume->source.input_file + ".3mf"; + ImGui::SetTooltip("Text configuation contain \n" + "Fix Transformation Matrix \n" + "%s\n" + "loaded from \"%s\" file.", + ss.str().c_str(), filename.c_str() + ); + } + } +#endif // SHOW_CONTAIN_3MF_FIX +#ifdef SHOW_ICONS_TEXTURE + auto &t = m_icons_texture; + ImGui::Image((void *) t.get_id(), ImVec2(t.get_width(), t.get_height())); +#endif //SHOW_ICONS_TEXTURE +#ifdef SHOW_IMGUI_ATLAS + const auto &atlas = m_style_manager.get_atlas(); + ImGui::Image(atlas.TexID, ImVec2(atlas.TexWidth, atlas.TexHeight)); +#endif // SHOW_IMGUI_ATLAS +} + +void GLGizmoEmboss::draw_text_input() +{ + auto create_range_text_prep = [&mng = m_style_manager, &text = m_text, &exist_unknown = m_text_contain_unknown_glyph]() { + auto& ff = mng.get_font_file_with_cache(); + assert(ff.has_value()); + const auto &cn = mng.get_font_prop().collection_number; + unsigned int font_index = (cn.has_value()) ? *cn : 0; + return create_range_text(text, *ff.font_file, font_index, &exist_unknown); + }; + + ImFont *imgui_font = m_style_manager.get_imgui_font(); + if (imgui_font == nullptr) { + // try create new imgui font + m_style_manager.create_imgui_font(create_range_text_prep()); + imgui_font = m_style_manager.get_imgui_font(); + } + bool exist_font = + imgui_font != nullptr && + imgui_font->IsLoaded() && + imgui_font->Scale > 0.f && + imgui_font->ContainerAtlas != nullptr; + // NOTE: Symbol fonts doesn't have atlas + // when their glyph range is out of language character range + if (exist_font) ImGui::PushFont(imgui_font); + + // show warning about incorrectness view of font + std::string warning; + std::string tool_tip; + if (!exist_font) { + warning = _u8L("Can't write text by selected font."); + tool_tip = _u8L("Try to choose another font."); + } else { + std::string who; + auto append_warning = [&who, &tool_tip](std::string w, std::string t) { + if (!w.empty()) { + if (!who.empty()) who += " & "; + who += w; + } + if (!t.empty()) { + if (!tool_tip.empty()) tool_tip += "\n"; + tool_tip += t; + } + }; + if (is_text_empty(m_text)) append_warning(_u8L("Empty"), _u8L("Embossed text can NOT contain only white spaces.")); + if (m_text_contain_unknown_glyph) + append_warning(_u8L("Bad symbol"), _u8L("Text contain character glyph (represented by '?') unknown by font.")); + + const FontProp &prop = m_style_manager.get_font_prop(); + if (prop.skew.has_value()) append_warning(_u8L("Skew"), _u8L("Unsupported visualization of font skew for text input.")); + if (prop.boldness.has_value()) append_warning(_u8L("Boldness"), _u8L("Unsupported visualization of font boldness for text input.")); + if (prop.line_gap.has_value()) + append_warning(_u8L("Line gap"), _u8L("Unsupported visualization of gap between lines inside text input.")); + auto &ff = m_style_manager.get_font_file_with_cache(); + float imgui_size = StyleManager::get_imgui_font_size(prop, *ff.font_file); + if (imgui_size > StyleManager::max_imgui_font_size) + append_warning(_u8L("To tall"), _u8L("Diminished font height inside text input.")); + if (imgui_size < StyleManager::min_imgui_font_size) + append_warning(_u8L("To small"), _u8L("Enlarged font height inside text input.")); + if (!who.empty()) warning = GUI::format(_L("%1% is NOT shown."), who); + } + + // add border around input when warning appears +#ifndef __APPLE__ + ScopeGuard input_border_sg; + if (!warning.empty()) { + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Border, ImGuiWrapper::COL_ORANGE_LIGHT); + input_border_sg = ScopeGuard([]() { ImGui::PopStyleColor(); ImGui::PopStyleVar(); }); + } +#endif + + // flag for extend font ranges if neccessary + // ranges can't be extend during font is activ(pushed) + std::string range_text; + float window_height = ImGui::GetWindowHeight(); + float minimal_height = get_minimal_window_size().y; + float extra_height = window_height - minimal_height; + ImVec2 text_size(m_gui_cfg->text_size.x, + m_gui_cfg->text_size.y + extra_height); + const ImGuiInputTextFlags flags = ImGuiInputTextFlags_AllowTabInput | ImGuiInputTextFlags_AutoSelectAll; + if (ImGui::InputTextMultiline("##Text", &m_text, text_size, flags)) { + process(); + range_text = create_range_text_prep(); + } + + if (exist_font) ImGui::PopFont(); + + if (!warning.empty()) { + if (ImGui::IsItemHovered() && !tool_tip.empty()) + ImGui::SetTooltip("%s", tool_tip.c_str()); + ImVec2 cursor = ImGui::GetCursorPos(); + float width = ImGui::GetContentRegionAvailWidth(); + ImVec2 size = ImGui::CalcTextSize(warning.c_str()); + ImVec2 padding = ImGui::GetStyle().FramePadding; + ImGui::SetCursorPos(ImVec2(width - size.x + padding.x, + cursor.y - size.y - padding.y)); + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, warning); + ImGui::SetCursorPos(cursor); + } + + // NOTE: must be after ImGui::font_pop() + // -> imgui_font has to be unused + // IMPROVE: only extend not clear + // Extend font ranges + if (!range_text.empty() && + !m_imgui->contain_all_glyphs(imgui_font, range_text) ) { + m_style_manager.clear_imgui_font(); + m_style_manager.create_imgui_font(range_text); + } +} + +#include +#include "wx/hashmap.h" +std::size_t hash_value(wxString const &s) +{ + boost::hash hasher; + return hasher(s.ToStdString()); +} + +static std::string concat(std::vector data) { + std::stringstream ss; + for (const auto &d : data) + ss << d.c_str() << ", "; + return ss.str(); +} + +#include +static boost::filesystem::path get_fontlist_cache_path() +{ + return boost::filesystem::path(data_dir()) / "cache" / "fonts.cereal"; +} + +// cache font list by cereal +#include +#include +#include +#include + +// increase number when change struct FacenamesSerializer +#define FACENAMES_VERSION 1 +struct FacenamesSerializer +{ + // hash number for unsorted vector of installed font into system + size_t hash = 0; + // assumption that is loadable + std::vector good; + // Can't load for some reason + std::vector bad; +}; + +template void save(Archive &archive, wxString const &d) +{ std::string s(d.ToUTF8().data()); archive(s);} +template void load(Archive &archive, wxString &d) +{ std::string s; archive(s); d = s;} + +template void serialize(Archive &ar, FacenamesSerializer &t, const std::uint32_t version) +{ + // When performing a load, the version associated with the class + // is whatever it was when that data was originally serialized + // When we save, we'll use the version that is defined in the macro + if (version != FACENAMES_VERSION) return; + ar(t.hash, t.good, t.bad); +} +CEREAL_CLASS_VERSION(FacenamesSerializer, FACENAMES_VERSION); // register class version + +#include +bool GLGizmoEmboss::store(const Facenames &facenames) { + std::string cache_path = get_fontlist_cache_path().string(); + boost::nowide::ofstream file(cache_path, std::ios::binary); + cereal::BinaryOutputArchive archive(file); + std::vector good; + good.reserve(facenames.faces.size()); + for (const FaceName &face : facenames.faces) good.push_back(face.wx_name); + FacenamesSerializer data = {facenames.hash, good, facenames.bad}; + + assert(std::is_sorted(data.bad.begin(), data.bad.end())); + assert(std::is_sorted(data.good.begin(), data.good.end())); + + try { + archive(data); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to write fontlist cache - " << cache_path << ex.what(); + return false; + } + return true; +} + +bool GLGizmoEmboss::load(Facenames &facenames) { + boost::filesystem::path path = get_fontlist_cache_path(); + std::string path_str = path.string(); + if (!boost::filesystem::exists(path)) { + BOOST_LOG_TRIVIAL(warning) << "Fontlist cache - '" << path_str << "' does not exists."; + return false; + } + boost::nowide::ifstream file(path_str, std::ios::binary); + cereal::BinaryInputArchive archive(file); + + FacenamesSerializer data; + try { + archive(data); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to load fontlist cache - '" << path_str << "'. Exception: " << ex.what(); + return false; + } + + assert(std::is_sorted(data.bad.begin(), data.bad.end())); + assert(std::is_sorted(data.good.begin(), data.good.end())); + + facenames.hash = data.hash; + facenames.faces.reserve(data.good.size()); + for (const wxString &face : data.good) + facenames.faces.push_back({face}); + facenames.bad = data.bad; + return true; +} + +void GLGizmoEmboss::init_face_names() { + Timer t("enumerate_fonts"); + if (m_face_names.is_init) return; + m_face_names.is_init = true; + + // to reload fonts from system, when install new one + wxFontEnumerator::InvalidateCache(); + + auto create_truncated_names = [&facenames = m_face_names, &width = m_gui_cfg->face_name_max_width]() { + for (FaceName &face : facenames.faces) { + std::string name_str(face.wx_name.ToUTF8().data()); + face.name_truncated = ImGuiWrapper::trunc(name_str, width); + } + }; + + // try load cache + // Only not OS enumerated face has hash value 0 + if (m_face_names.hash == 0) { + load(m_face_names); + create_truncated_names(); + } + + using namespace std::chrono; + steady_clock::time_point enumerate_start = steady_clock::now(); + ScopeGuard sg([&enumerate_start, &face_names = m_face_names]() { + steady_clock::time_point enumerate_end = steady_clock::now(); + long long enumerate_duration = duration_cast(enumerate_end - enumerate_start).count(); + BOOST_LOG_TRIVIAL(info) << "OS enumerate " << face_names.faces.size() << " fonts " + << "(+ " << face_names.bad.size() << " can't load " + << "= " << face_names.faces.size() + face_names.bad.size() << " fonts) " + << "in " << enumerate_duration << " ms\n" << concat(face_names.bad); + }); + wxArrayString facenames = wxFontEnumerator::GetFacenames(m_face_names.encoding); + size_t hash = boost::hash_range(facenames.begin(), facenames.end()); + // Zero value is used as uninitialized hash + if (hash == 0) hash = 1; + // check if it is same as last time + if (m_face_names.hash == hash) { + // no new installed font + BOOST_LOG_TRIVIAL(info) << "Same FontNames hash, cache is used. " + << "For clear cache delete file: " << get_fontlist_cache_path().string(); + return; + } + + BOOST_LOG_TRIVIAL(info) << ((m_face_names.hash == 0) ? + "FontName list is generate from scratch." : + "Hash are different. Only previous bad fonts are used and set again as bad"); + m_face_names.hash = hash; + + // validation lambda + auto is_valid_font = [encoding = m_face_names.encoding, bad = m_face_names.bad /*copy*/](const wxString &name) { + if (name.empty()) return false; + + // vertical font start with @, we will filter it out + // Not sure if it is only in Windows so filtering is on all platforms + if (name[0] == '@') return false; + + // previously detected bad font + auto it = std::lower_bound(bad.begin(), bad.end(), name); + if (it != bad.end() && *it == name) return false; + + wxFont wx_font(wxFontInfo().FaceName(name).Encoding(encoding)); + //* + // Faster chech if wx_font is loadable but not 100% + // names could contain not loadable font + if (!WxFontUtils::can_load(wx_font)) return false; + + /*/ + // Slow copy of font files to try load font + // After this all files are loadable + auto font_file = WxFontUtils::create_font_file(wx_font); + if (font_file == nullptr) + return false; // can't create font file + // */ + return true; + }; + + m_face_names.faces.clear(); + m_face_names.bad.clear(); + m_face_names.faces.reserve(facenames.size()); + std::sort(facenames.begin(), facenames.end()); + for (const wxString &name : facenames) { + if (is_valid_font(name)) { + m_face_names.faces.push_back({name}); + }else{ + m_face_names.bad.push_back(name); + } + } + assert(std::is_sorted(m_face_names.bad.begin(), m_face_names.bad.end())); + create_truncated_names(); + store(m_face_names); +} + +// create texture for visualization font face +void GLGizmoEmboss::init_font_name_texture() { + Timer t("init_font_name_texture"); + // check if already exists + GLuint &id = m_face_names.texture_id; + if (id != 0) return; + // create texture for font + GLenum target = GL_TEXTURE_2D; + glsafe(::glGenTextures(1, &id)); + glsafe(::glBindTexture(target, id)); + glsafe(::glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); + glsafe(::glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); + const Vec2i &size = m_gui_cfg->face_name_size; + GLint w = size.x(), h = m_face_names.count_cached_textures * size.y(); + std::vector data(4*w * h, {0}); + const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; + const GLint level = 0, internal_format = GL_RGBA, border = 0; + glsafe(::glTexImage2D(target, level, internal_format, w, h, border, format, type, data.data())); + + // bind default texture + GLuint no_texture_id = 0; + glsafe(::glBindTexture(target, no_texture_id)); + + // clear info about creation of texture - no one is initialized yet + for (FaceName &face : m_face_names.faces) { + face.cancel = nullptr; + face.is_created = nullptr; + } +} + +void GLGizmoEmboss::draw_font_preview(FaceName& face, bool is_visible) +{ + unsigned int &count_opened_fonts = m_face_names.count_opened_font_files; + ImVec2 size(m_gui_cfg->face_name_size.x(), m_gui_cfg->face_name_size.y()); + // set to pixel 0,0 in texture + ImVec2 uv0(0.f, 0.f), uv1(1.f / size.x, 1.f / size.y / m_face_names.count_cached_textures); + ImTextureID tex_id = (void *) (intptr_t) m_face_names.texture_id; + if (face.is_created != nullptr) { + if (*face.is_created) { + size_t texture_index = face.texture_index; + uv0 = ImVec2(0.f, texture_index / (float) m_face_names.count_cached_textures), + uv1 = ImVec2(1.f, (texture_index + 1) / (float) m_face_names.count_cached_textures); + } else if (!is_visible) { + face.is_created = nullptr; + face.cancel->store(true); + } + } else if (is_visible && count_opened_fonts < m_gui_cfg->max_count_opened_font_files) { + ++count_opened_fonts; + face.cancel = std::make_shared(false); + face.is_created = std::make_shared(false); + + std::string text = m_text.empty() ? "AaBbCc" : m_text; + + const unsigned char gray_level = 5; + // format type and level must match to texture data + const GLenum format = GL_RGBA, type = GL_UNSIGNED_BYTE; + const GLint level = 0; + // select next texture index + size_t texture_index = (m_face_names.texture_index + 1) % m_face_names.count_cached_textures; + // set previous cach as deleted + for (FaceName &f : m_face_names.faces) + if (f.texture_index == texture_index) { + if (f.cancel != nullptr) f.cancel->store(true); + f.is_created = nullptr; + } + + m_face_names.texture_index = texture_index; + face.texture_index = texture_index; + + // clear texture + + + // render text to texture + FontImageData data{ + text, + face.wx_name, + m_face_names.encoding, + m_face_names.texture_id, + m_face_names.texture_index, + m_gui_cfg->face_name_size, + gray_level, + format, + type, + level, + &count_opened_fonts, + face.cancel, // copy + face.is_created // copy + }; + auto job = std::make_unique(std::move(data)); + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + queue_job(worker, std::move(job)); + } + + ImGui::SameLine(m_gui_cfg->face_name_texture_offset_x); + ImGui::Image(tex_id, size, uv0, uv1); +} + +bool GLGizmoEmboss::select_facename(const wxString &facename) +{ + if (!wxFontEnumerator::IsValidFacename(facename)) return false; + // Select font + const wxFontEncoding &encoding = m_face_names.encoding; + wxFont wx_font(wxFontInfo().FaceName(facename).Encoding(encoding)); + if (!wx_font.IsOk()) return false; + // wx font could change source file by size of font + int point_size = static_cast(m_style_manager.get_font_prop().size_in_mm); + wx_font.SetPointSize(point_size); + if (!m_style_manager.set_wx_font(wx_font)) return false; + process(); + return true; +} + +void GLGizmoEmboss::draw_font_list() +{ + // Set partial + wxString actual_face_name; + if (m_style_manager.is_activ_font()) { + const std::optional &wx_font_opt = m_style_manager.get_wx_font(); + if (wx_font_opt.has_value()) + actual_face_name = wx_font_opt->GetFaceName(); + } + // name of actual selected font + const char * selected = (!actual_face_name.empty()) ? + actual_face_name.ToUTF8().data() : " --- "; + + // Do not remove font face during enumeration + // When deletation of font appear this variable is set + std::optional del_index; + + // When is unknown font is inside .3mf only font selection is allowed + // Stop Imgui disable + Guard again start disabling + ScopeGuard unknown_font_sc; + if (m_is_unknown_font) { + m_imgui->disabled_end(); + unknown_font_sc = ScopeGuard([&]() { + m_imgui->disabled_begin(true); + }); + } + + if (ImGui::BeginCombo("##font_selector", selected)) { + if (!m_face_names.is_init) init_face_names(); + if (m_face_names.texture_id == 0) init_font_name_texture(); + for (FaceName &face : m_face_names.faces) { + const wxString &wx_face_name = face.wx_name; + size_t index = &face - &m_face_names.faces.front(); + ImGui::PushID(index); + ScopeGuard sg([]() { ImGui::PopID(); }); + bool is_selected = (actual_face_name == wx_face_name); + ImVec2 selectable_size(0, m_gui_cfg->face_name_size.y()); + ImGuiSelectableFlags flags = 0; + if (ImGui::Selectable(face.name_truncated.c_str(), is_selected, flags, selectable_size)) { + if (!select_facename(wx_face_name)) { + del_index = index; + wxMessageBox(GUI::format(_L("Font face \"%1%\" can't be selected."), wx_face_name)); + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", wx_face_name.ToUTF8().data()); + if (is_selected) ImGui::SetItemDefaultFocus(); + draw_font_preview(face, ImGui::IsItemVisible()); + } +#ifdef SHOW_FONT_COUNT + ImGui::TextColored(ImGuiWrapper::COL_GREY_LIGHT, "Count %d", + static_cast(m_face_names.names.size())); +#endif // SHOW_FONT_COUNT + ImGui::EndCombo(); + } else if (m_face_names.is_init) { + // Just one after close combo box + // free texture and set id to zero + + m_face_names.is_init = false; + // cancel all process for generation of texture + for (FaceName &face : m_face_names.faces) + if (face.cancel != nullptr) face.cancel->store(true); + glsafe(::glDeleteTextures(1, &m_face_names.texture_id)); + m_face_names.texture_id = 0; + } + + // delete unloadable face name when try to use + if (del_index.has_value()) { + auto face = m_face_names.faces.begin() + (*del_index); + std::vector& bad = m_face_names.bad; + // sorted insert into bad fonts + auto it = std::upper_bound(bad.begin(), bad.end(), face->wx_name); + bad.insert(it, face->wx_name); + m_face_names.faces.erase(face); + // update cached file + store(m_face_names); + } + +#ifdef ALLOW_ADD_FONT_BY_FILE + ImGui::SameLine(); + // select font file by file browser + if (draw_button(IconType::open_file)) { + if (choose_true_type_file()) { + process(); + } + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("add file with font(.ttf, .ttc)").c_str()); +#endif // ALLOW_ADD_FONT_BY_FILE + +#ifdef ALLOW_ADD_FONT_BY_OS_SELECTOR + ImGui::SameLine(); + if (draw_button(IconType::system_selector)) { + if (choose_font_by_wxdialog()) { + process(); + } + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Open dialog for choose from fonts.").c_str()); +#endif // ALLOW_ADD_FONT_BY_OS_SELECTOR + +} + +void GLGizmoEmboss::draw_model_type() +{ + bool is_last_solid_part = is_text_object(m_volume); + const char * label = m_gui_cfg->translations.type.c_str(); + if (is_last_solid_part) { + ImVec4 color{.5f, .5f, .5f, 1.f}; + m_imgui->text_colored(color, label); + } else { + ImGui::Text("%s", label); + } + ImGui::SameLine(m_gui_cfg->style_offset); + + std::optional new_type; + ModelVolumeType modifier = ModelVolumeType::PARAMETER_MODIFIER; + ModelVolumeType negative = ModelVolumeType::NEGATIVE_VOLUME; + ModelVolumeType part = ModelVolumeType::MODEL_PART; + ModelVolumeType type = m_volume->type(); + + if (type == part) { + draw_icon(IconType::part, IconState::hovered); + } else { + if (draw_button(IconType::part)) new_type = part; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Click to change text into object part.").c_str()); + } + + ImGui::SameLine(); + if (type == negative) { + draw_icon(IconType::negative, IconState::hovered); + } else { + if (draw_button(IconType::negative, is_last_solid_part)) + new_type = negative; + if(ImGui::IsItemHovered()){ + if(is_last_solid_part) + ImGui::SetTooltip("%s", _u8L("You can't change a type of the last solid part of the object.").c_str()); + else if (type != negative) + ImGui::SetTooltip("%s", _u8L("Click to change part type into negative volume.").c_str()); + } + } + + ImGui::SameLine(); + if (type == modifier) { + draw_icon(IconType::modifier, IconState::hovered); + } else { + if(draw_button(IconType::modifier, is_last_solid_part)) + new_type = modifier; + if (ImGui::IsItemHovered()) { + if(is_last_solid_part) + ImGui::SetTooltip("%s", _u8L("You can't change a type of the last solid part of the object.").c_str()); + else if (type != modifier) + ImGui::SetTooltip("%s", _u8L("Click to change part type into modifier.").c_str()); + } + } + + if (m_volume != nullptr && new_type.has_value() && !is_last_solid_part) { + GUI_App &app = wxGetApp(); + Plater * plater = app.plater(); + Plater::TakeSnapshot snapshot(plater, _L("Change Text Type"), UndoRedo::SnapshotType::GizmoAction); + m_volume->set_type(*new_type); + + // Update volume position when switch from part or into part + if (m_volume->text_configuration->style.prop.use_surface) { + // move inside + bool is_volume_move_inside = (type == part); + bool is_volume_move_outside = (*new_type == part); + if (is_volume_move_inside || is_volume_move_outside) process(); + } + + // inspiration in ObjectList::change_part_type() + // how to view correct side panel with objects + ObjectList *obj_list = app.obj_list(); + wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection( + obj_list->get_selected_obj_idx(), + [volume = m_volume](const ModelVolume *vol) { return vol == volume; }); + if (!sel.IsEmpty()) obj_list->select_item(sel.front()); + + // NOTE: on linux, function reorder_volumes_and_get_selection call GLCanvas3D::reload_scene(refresh_immediately = false) + // which discard m_volume pointer and set it to nullptr also selection is cleared so gizmo is automaticaly closed + auto &mng = m_parent.get_gizmos_manager(); + if (mng.get_current_type() != GLGizmosManager::Emboss) + mng.open_gizmo(GLGizmosManager::Emboss); + // TODO: select volume back - Ask @Sasa + } +} + +void GLGizmoEmboss::draw_style_rename_popup() { + std::string& new_name = m_style_manager.get_style().name; + const std::string &old_name = m_style_manager.get_stored_style()->name; + std::string text_in_popup = GUI::format(_L("Rename style(%1%) for embossing text: "), old_name); + ImGui::Text("%s", text_in_popup.c_str()); + + bool is_unique = true; + for (const auto &item : m_style_manager.get_styles()) { + const EmbossStyle &style = item.style; + if (&style == &m_style_manager.get_style()) + continue; // could be same as original name + if (style.name == new_name) is_unique = false; + } + bool allow_change = false; + if (new_name.empty()) { + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name can't be empty.")); + }else if (!is_unique) { + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name has to be unique.")); + } else { + ImGui::NewLine(); + allow_change = true; + } + + bool store = false; + ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue; + if (ImGui::InputText("##rename style", &new_name, flags) && allow_change) store = true; + if (m_imgui->button(_L("ok"), ImVec2(0.f, 0.f), allow_change)) store = true; + ImGui::SameLine(); + if (ImGui::Button(_u8L("cancel").c_str())) { + new_name = old_name; + ImGui::CloseCurrentPopup(); + } + + if (store) { + // rename style in all objects and volumes + for (ModelObject *mo :wxGetApp().plater()->model().objects) { + for (ModelVolume *mv : mo->volumes) { + if (!mv->text_configuration.has_value()) continue; + std::string& name = mv->text_configuration->style.name; + if (name != old_name) continue; + name = new_name; + } + } + + m_style_manager.rename(new_name); + m_style_manager.store_styles_to_app_config(); + ImGui::CloseCurrentPopup(); + } +} + +void GLGizmoEmboss::draw_style_rename_button() +{ + bool can_rename = m_style_manager.exist_stored_style(); + std::string title = _u8L("Rename style"); + const char * popup_id = title.c_str(); + if (draw_button(IconType::rename, !can_rename)) { + assert(m_style_manager.get_stored_style()); + ImGui::OpenPopup(popup_id); + } + else if (ImGui::IsItemHovered()) { + if (can_rename) ImGui::SetTooltip("%s", _u8L("Rename actual style.").c_str()); + else ImGui::SetTooltip("%s", _u8L("Can't rename temporary style.").c_str()); + } + if (ImGui::BeginPopupModal(popup_id, 0, ImGuiWindowFlags_AlwaysAutoResize)) { + draw_style_rename_popup(); + ImGui::EndPopup(); + } +} + +void GLGizmoEmboss::draw_style_save_button(bool is_modified) +{ + if (draw_button(IconType::save, !is_modified)) { + // save styles to app config + m_style_manager.store_styles_to_app_config(); + }else if (ImGui::IsItemHovered()) { + std::string tooltip; + if (!m_style_manager.exist_stored_style()) { + tooltip = _u8L("First Add style to list."); + } else if (is_modified) { + tooltip = GUI::format(_L("Save %1% style"), m_style_manager.get_style().name); + } else { + tooltip = _u8L("No changes to save."); + } + ImGui::SetTooltip("%s", tooltip.c_str()); + } +} + +void GLGizmoEmboss::draw_style_save_as_popup() { + ImGui::Text("%s", _u8L("New name of style: ").c_str()); + + // use name inside of volume configuration as temporary new name + std::string &new_name = m_volume->text_configuration->style.name; + + bool is_unique = true; + for (const auto &item : m_style_manager.get_styles()) + if (item.style.name == new_name) is_unique = false; + + bool allow_change = false; + if (new_name.empty()) { + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name can't be empty.")); + }else if (!is_unique) { + m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_DARK, _u8L("Name has to be unique.")); + } else { + ImGui::NewLine(); + allow_change = true; + } + + bool save_style = false; + ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue; + if (ImGui::InputText("##save as style", &new_name, flags)) + save_style = true; + + if (m_imgui->button(_L("ok"), ImVec2(0.f, 0.f), allow_change)) + save_style = true; + + ImGui::SameLine(); + if (ImGui::Button(_u8L("cancel").c_str())){ + // write original name to volume TextConfiguration + new_name = m_style_manager.get_style().name; + ImGui::CloseCurrentPopup(); + } + + if (save_style && allow_change) { + m_style_manager.add_style(new_name); + m_style_manager.store_styles_to_app_config(); + ImGui::CloseCurrentPopup(); + } +} + +void GLGizmoEmboss::draw_style_add_button() +{ + bool only_add_style = !m_style_manager.exist_stored_style(); + bool can_add = true; + if (only_add_style && + m_volume->text_configuration->style.type != WxFontUtils::get_actual_type()) + can_add = false; + + std::string title = _u8L("Save as new style"); + const char *popup_id = title.c_str(); + // save as new style + ImGui::SameLine(); + if (draw_button(IconType::add, !can_add)) { + if (!m_style_manager.exist_stored_style()) { + m_style_manager.store_styles_to_app_config(wxGetApp().app_config); + } else { + ImGui::OpenPopup(popup_id); + } + } else if (ImGui::IsItemHovered()) { + if (!can_add) { + ImGui::SetTooltip("%s", _u8L("Only valid font can be added to style.").c_str()); + }else if (only_add_style) { + ImGui::SetTooltip("%s", _u8L("Add style to my list.").c_str()); + } else { + ImGui::SetTooltip("%s", _u8L("Add as new named style.").c_str()); + } + } + + if (ImGui::BeginPopupModal(popup_id, 0, ImGuiWindowFlags_AlwaysAutoResize)) { + draw_style_save_as_popup(); + ImGui::EndPopup(); + } +} + +void GLGizmoEmboss::draw_delete_style_button() { + bool is_stored = m_style_manager.exist_stored_style(); + bool is_last = m_style_manager.get_styles().size() == 1; + bool can_delete = is_stored && !is_last; + + std::string title = _u8L("Remove style"); + const char * popup_id = title.c_str(); + static size_t next_style_index = std::numeric_limits::max(); + if (draw_button(IconType::erase, !can_delete)) { + while (true) { + // NOTE: can't use previous loaded activ index -> erase could change index + size_t activ_index = m_style_manager.get_style_index(); + next_style_index = (activ_index > 0) ? activ_index - 1 : + activ_index + 1; + if (next_style_index >= m_style_manager.get_styles().size()) { + // can't remove last font style + // TODO: inform user + break; + } + // IMPROVE: add function can_load? + // clean unactivable styles + if (!m_style_manager.load_style(next_style_index)) { + m_style_manager.erase(next_style_index); + continue; + } + + // load back + m_style_manager.load_style(activ_index); + ImGui::OpenPopup(popup_id); + break; + } + } + + if (ImGui::IsItemHovered()) { + const std::string &style_name = m_style_manager.get_style().name; + std::string tooltip; + if (can_delete) tooltip = GUI::format(_L("Delete \"%1%\" style."), style_name); + else if (is_last) tooltip = GUI::format(_L("Can't delete \"%1%\". It is last style."), style_name); + else/*if(!is_stored)*/ tooltip = GUI::format(_L("Can't delete temporary style \"%1%\"."), style_name); + ImGui::SetTooltip("%s", tooltip.c_str()); + } + + if (ImGui::BeginPopupModal(popup_id)) { + const std::string &style_name = m_style_manager.get_style().name; + std::string text_in_popup = GUI::format(_L("Are you sure,\nthat you want permanently and unrecoverable \nremove style \"%1%\"?"), style_name); + ImGui::Text("%s", text_in_popup.c_str()); + if (ImGui::Button(_u8L("Yes").c_str())) { + size_t activ_index = m_style_manager.get_style_index(); + m_style_manager.load_style(next_style_index); + m_style_manager.erase(activ_index); + m_style_manager.store_styles_to_app_config(wxGetApp().app_config); + ImGui::CloseCurrentPopup(); + process(); + } + ImGui::SameLine(); + if (ImGui::Button(_u8L("No").c_str())) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } +} + +void GLGizmoEmboss::fix_transformation(const FontProp &from, + const FontProp &to) +{ + // fix Z rotation when exists difference in styles + const std::optional &f_angle_opt = from.angle; + const std::optional &t_angle_opt = to.angle; + if (!is_approx(f_angle_opt, t_angle_opt)) { + // fix rotation + float f_angle = f_angle_opt.has_value() ? *f_angle_opt : .0f; + float t_angle = t_angle_opt.has_value() ? *t_angle_opt : .0f; + do_rotate(t_angle - f_angle); + } + + // fix distance (Z move) when exists difference in styles + const std::optional &f_move_opt = from.distance; + const std::optional &t_move_opt = to.distance; + if (!is_approx(f_move_opt, t_move_opt)) { + float f_move = f_move_opt.has_value() ? *f_move_opt : .0f; + float t_move = t_move_opt.has_value() ? *t_move_opt : .0f; + do_translate(Vec3d::UnitZ() * (t_move - f_move)); + } +} + +void GLGizmoEmboss::draw_style_list() { + if (!m_style_manager.is_activ_font()) return; + + const EmbossStyle *stored_style = nullptr; + bool is_stored = m_style_manager.exist_stored_style(); + if (is_stored) + stored_style = m_style_manager.get_stored_style(); + const EmbossStyle &actual_style = m_style_manager.get_style(); + bool is_changed = (stored_style)? !(*stored_style == actual_style) : true; + bool is_modified = is_stored && is_changed; + + const float &max_style_name_width = m_gui_cfg->max_style_name_width; + std::string &trunc_name = m_style_manager.get_truncated_name(); + if (trunc_name.empty()) { + // generate trunc name + std::string current_name = actual_style.name; + ImGuiWrapper::escape_double_hash(current_name); + trunc_name = ImGuiWrapper::trunc(current_name, max_style_name_width); + } + + if (m_style_manager.exist_stored_style()) + ImGui::Text("%s", m_gui_cfg->translations.style.c_str()); + else ImGui::TextColored(ImGuiWrapper::COL_ORANGE_LIGHT, "%s", m_gui_cfg->translations.style.c_str()); + + ImGui::SameLine(m_gui_cfg->style_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + auto add_text_modify = [&is_modified](const std::string& name) { + if (!is_modified) return name; + return name + " (" + _u8L("modified") + ")"; + }; + std::optional selected_style_index; + if (ImGui::BeginCombo("##style_selector", add_text_modify(trunc_name).c_str())) { + m_style_manager.init_style_images(m_gui_cfg->max_style_image_size, m_text); + m_style_manager.init_trunc_names(max_style_name_width); + std::optional> swap_indexes; + const std::vector &styles = m_style_manager.get_styles(); + for (const auto &item : styles) { + size_t index = &item - &styles.front(); + const EmbossStyle &style = item.style; + const std::string &actual_style_name = style.name; + ImGui::PushID(actual_style_name.c_str()); + bool is_selected = (index == m_style_manager.get_style_index()); + + ImVec2 select_size(0,m_gui_cfg->max_style_image_size.y()); // 0,0 --> calculate in draw + const std::optional &img = item.image; + // allow click delete button + ImGuiSelectableFlags_ flags = ImGuiSelectableFlags_AllowItemOverlap; + if (ImGui::Selectable(item.truncated_name.c_str(), is_selected, flags, select_size)) { + selected_style_index = index; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", actual_style_name.c_str()); + + // reorder items + if (ImGui::IsItemActive() && !ImGui::IsItemHovered()) { + if (ImGui::GetMouseDragDelta(0).y < 0.f) { + if (index > 0) + swap_indexes = {index, index - 1}; + } else if ((index + 1) < styles.size()) + swap_indexes = {index, index + 1}; + if (swap_indexes.has_value()) + ImGui::ResetMouseDragDelta(); + } + + // draw style name + if (img.has_value()) { + ImGui::SameLine(max_style_name_width); + ImGui::Image(img->texture_id, img->tex_size, img->uv0, img->uv1); + } + + ImGui::PopID(); + } + if (swap_indexes.has_value()) + m_style_manager.swap(swap_indexes->first, + swap_indexes->second); + ImGui::EndCombo(); + } else { + // do not keep in memory style images when no combo box open + m_style_manager.free_style_images(); + if (ImGui::IsItemHovered()) { + std::string style_name = add_text_modify(actual_style.name); + std::string tooltip = is_modified? + GUI::format(_L("Modified style \"%1%\""), actual_style.name): + GUI::format(_L("Current style is \"%1%\""), actual_style.name); + ImGui::SetTooltip(" %s", tooltip.c_str()); + } + } + + // Check whether user wants lose actual style modification + if (selected_style_index.has_value() && is_modified) { + wxString title = _L("Style modification will be lost."); + const EmbossStyle &style = m_style_manager.get_styles()[*selected_style_index].style; + wxString message = GUI::format_wxstr(_L("Changing style to '%1%' will discard actual style modification.\n\n Would you like to continue anyway?"), style.name); + MessageDialog not_loaded_style_message(nullptr, message, title, wxICON_WARNING | wxYES|wxNO); + if (not_loaded_style_message.ShowModal() != wxID_YES) + selected_style_index.reset(); + } + + // selected style from combo box + if (selected_style_index.has_value()) { + const EmbossStyle &style = m_style_manager.get_styles()[*selected_style_index].style; + fix_transformation(actual_style.prop, style.prop); + if (m_style_manager.load_style(*selected_style_index)) { + process(); + } else { + wxString title = _L("Not valid style."); + wxString message = GUI::format_wxstr(_L("Style '%1%' can't be used and will be removed from list."), style.name); + MessageDialog not_loaded_style_message(nullptr, message, title, wxOK); + not_loaded_style_message.ShowModal(); + m_style_manager.erase(*selected_style_index); + } + } + + ImGui::SameLine(); + draw_style_rename_button(); + + ImGui::SameLine(); + draw_style_save_button(is_modified); + + ImGui::SameLine(); + draw_style_add_button(); + + // delete button + ImGui::SameLine(); + draw_delete_style_button(); +} + +bool GLGizmoEmboss::draw_italic_button() +{ + const std::optional &wx_font_opt = m_style_manager.get_wx_font(); + const auto& ff = m_style_manager.get_font_file_with_cache(); + if (!wx_font_opt.has_value() || !ff.has_value()) { + draw_icon(IconType::italic, IconState::disabled); + return false; + } + const wxFont& wx_font = *wx_font_opt; + + std::optional &skew = m_style_manager.get_font_prop().skew; + bool is_font_italic = skew.has_value() || WxFontUtils::is_italic(wx_font); + if (is_font_italic) { + // unset italic + if (draw_clickable(IconType::italic, IconState::hovered, + IconType::unitalic, IconState::hovered)) { + skew.reset(); + if (wx_font.GetStyle() != wxFontStyle::wxFONTSTYLE_NORMAL) { + wxFont new_wx_font = wx_font; // copy + new_wx_font.SetStyle(wxFontStyle::wxFONTSTYLE_NORMAL); + if(!m_style_manager.set_wx_font(new_wx_font)) + return false; + } + return true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Unset italic").c_str()); + } else { + // set italic + if (draw_button(IconType::italic)) { + wxFont new_wx_font = wx_font; // copy + auto new_ff = WxFontUtils::set_italic(new_wx_font, *ff.font_file); + if (new_ff != nullptr) { + if(!m_style_manager.set_wx_font(new_wx_font, std::move(new_ff))) + return false; + } else { + // italic font doesn't exist + // add skew when wxFont can't set it + skew = 0.2f; + } + return true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Set italic").c_str()); + } + return false; +} + +bool GLGizmoEmboss::draw_bold_button() { + const std::optional &wx_font_opt = m_style_manager.get_wx_font(); + const auto& ff = m_style_manager.get_font_file_with_cache(); + if (!wx_font_opt.has_value() || !ff.has_value()) { + draw_icon(IconType::bold, IconState::disabled); + return false; + } + const wxFont &wx_font = *wx_font_opt; + + std::optional &boldness = m_style_manager.get_font_prop().boldness; + bool is_font_bold = boldness.has_value() || WxFontUtils::is_bold(wx_font); + if (is_font_bold) { + // unset bold + if (draw_clickable(IconType::bold, IconState::hovered, + IconType::unbold, IconState::hovered)) { + boldness.reset(); + if (wx_font.GetWeight() != wxFontWeight::wxFONTWEIGHT_NORMAL) { + wxFont new_wx_font = wx_font; // copy + new_wx_font.SetWeight(wxFontWeight::wxFONTWEIGHT_NORMAL); + if(!m_style_manager.set_wx_font(new_wx_font)) + return false; + } + return true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Unset bold").c_str()); + } else { + // set bold + if (draw_button(IconType::bold)) { + wxFont new_wx_font = wx_font; // copy + auto new_ff = WxFontUtils::set_bold(new_wx_font, *ff.font_file); + if (new_ff != nullptr) { + if(!m_style_manager.set_wx_font(new_wx_font, std::move(new_ff))) + return false; + } else { + // bold font can't be loaded + // set up boldness + boldness = 20.f; + //font_file->cache.empty(); + } + return true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Set bold").c_str()); + } + return false; +} + +template bool exist_change(const T &value, const T *default_value){ + if (default_value == nullptr) return false; + return (value != *default_value); +} + +template<> bool exist_change(const std::optional &value, const std::optional *default_value){ + if (default_value == nullptr) return false; + return !is_approx(value, *default_value); +} + +template<> bool exist_change(const float &value, const float *default_value){ + if (default_value == nullptr) return false; + return !is_approx(value, *default_value); +} + +template +bool GLGizmoEmboss::revertible(const std::string &name, + T &value, + const T *default_value, + const std::string &undo_tooltip, + float undo_offset, + Draw draw) +{ + bool changed = exist_change(value, default_value); + if (changed || default_value == nullptr) + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, name); + else + ImGuiWrapper::text(name); + + bool result = draw(); + // render revert changes button + if (changed) { + ImGui::SameLine(undo_offset); + if (draw_button(IconType::undo)) { + value = *default_value; + return true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", undo_tooltip.c_str()); + } + return result; +} + + +bool GLGizmoEmboss::rev_input(const std::string &name, + float &value, + const float *default_value, + const std::string &undo_tooltip, + float step, + float step_fast, + const char *format, + ImGuiInputTextFlags flags) +{ + // draw offseted input + auto draw_offseted_input = [&]()->bool{ + float input_offset = m_gui_cfg->input_offset; + float input_width = m_gui_cfg->input_width; + ImGui::SameLine(input_offset); + ImGui::SetNextItemWidth(input_width); + return ImGui::InputFloat(("##" + name).c_str(), + &value, step, step_fast, format, flags); + }; + float undo_offset = ImGui::GetStyle().FramePadding.x; + return revertible(name, value, default_value, undo_tooltip, undo_offset, draw_offseted_input); +} + +bool GLGizmoEmboss::rev_checkbox(const std::string &name, + bool &value, + const bool *default_value, + const std::string &undo_tooltip) +{ + // draw offseted input + auto draw_offseted_input = [&]() -> bool { + ImGui::SameLine(m_gui_cfg->advanced_input_offset); + return ImGui::Checkbox(("##" + name).c_str(), &value); + }; + float undo_offset = ImGui::GetStyle().FramePadding.x; + return revertible(name, value, default_value, undo_tooltip, + undo_offset, draw_offseted_input); +} + +void GLGizmoEmboss::draw_style_edit() { + const GuiCfg::Translations &tr = m_gui_cfg->translations; + + const std::optional &wx_font_opt = m_style_manager.get_wx_font(); + EmbossStyle &style = m_style_manager.get_style(); + + assert(wx_font_opt.has_value()); + if (!wx_font_opt.has_value()) { + ImGui::TextColored(ImGuiWrapper::COL_ORANGE_DARK, "%s", _u8L("WxFont is not loaded properly.").c_str()); + return; + } + + bool exist_stored_style = m_style_manager.exist_stored_style(); + bool is_font_changed = false; + if (exist_stored_style && wx_font_opt.has_value()) { + const wxFont &wx_font = *wx_font_opt; + const EmbossStyle *stored_style = m_style_manager.get_stored_style(); + assert(stored_style != nullptr); + const std::optional &stored_wx = m_style_manager.get_stored_wx_font(); + assert(stored_wx.has_value()); + bool is_font_face_changed = stored_wx->GetFaceName() != wx_font.GetFaceName(); + + const std::optional &skew = m_style_manager.get_font_prop().skew; + bool is_italic = skew.has_value() || WxFontUtils::is_italic(wx_font); + const std::optional &skew_stored = stored_style->prop.skew; + bool is_stored_italic = skew_stored.has_value() || WxFontUtils::is_italic(*stored_wx); + bool is_italic_changed = is_italic != is_stored_italic; + + const std::optional &boldness = m_style_manager.get_font_prop().boldness; + bool is_bold = boldness.has_value() || WxFontUtils::is_bold(wx_font); + const std::optional &boldness_stored = stored_style->prop.boldness; + bool is_stored_bold = boldness_stored.has_value() || WxFontUtils::is_bold(*stored_wx); + bool is_bold_changed = is_bold != is_stored_bold; + + bool is_font_style_changed = is_italic_changed || is_bold_changed; + + is_font_changed = is_font_face_changed || is_font_style_changed; + } + + if (is_font_changed || !exist_stored_style) + ImGuiWrapper::text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, tr.font); + else + ImGuiWrapper::text(tr.font); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + draw_font_list(); + ImGui::SameLine(); + bool exist_change = false; + if (draw_italic_button()) exist_change = true; + + ImGui::SameLine(); + if (draw_bold_button()) exist_change = true; + + if (is_font_changed) { + ImGui::SameLine(ImGui::GetStyle().FramePadding.x); + if (draw_button(IconType::undo)) { + const EmbossStyle *stored_style = m_style_manager.get_stored_style(); + style.path = stored_style->path; + style.prop.boldness = stored_style->prop.boldness; + style.prop.skew = stored_style->prop.skew; + + wxFont new_wx_font = WxFontUtils::load_wxFont(style.path); + if (new_wx_font.IsOk() && + m_style_manager.set_wx_font(new_wx_font)) + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Revert font changes.").c_str()); + } + + if (exist_change) { + m_style_manager.clear_glyphs_cache(); + process(); + } + + bool use_inch = wxGetApp().app_config->get("use_inches") == "1"; + const std::string revert_text_size = _u8L("Revert text size."); + FontProp &font_prop = style.prop; + const float * def_size = exist_stored_style? + &m_style_manager.get_stored_style()->prop.size_in_mm : nullptr; + bool is_size_changed = false; + if (use_inch) { + float size_in_inch = ObjectManipulation::mm_to_in * font_prop.size_in_mm; + float def_size_inch = exist_stored_style ? ObjectManipulation::mm_to_in * (*def_size) : 0.f; + if (def_size != nullptr) def_size = &def_size_inch; + if (rev_input(tr.size, size_in_inch, def_size, revert_text_size, 0.1f, 1.f, "%.2f in")) { + font_prop.size_in_mm = ObjectManipulation::in_to_mm * size_in_inch; + is_size_changed = true; + } + } else { + if (rev_input(tr.size, font_prop.size_in_mm, def_size, revert_text_size, 0.1f, 1.f, "%.1f mm")) + is_size_changed = true; + } + + if (is_size_changed) { + // size can't be zero or negative + Limits::apply(font_prop.size_in_mm, limits.size_in_mm); + + // only different value need process + if (!is_approx(font_prop.size_in_mm, m_volume->text_configuration->style.prop.size_in_mm)) { + // store font size into path + if (style.type == WxFontUtils::get_actual_type()) { + if (wx_font_opt.has_value()) { + wxFont wx_font = *wx_font_opt; + wx_font.SetPointSize(static_cast(font_prop.size_in_mm)); + m_style_manager.set_wx_font(wx_font); + } + } + process(); + } + } + +#ifdef SHOW_WX_WEIGHT_INPUT + if (wx_font.has_value()) { + ImGui::Text("%s", "weight"); + ImGui::SameLine(m_gui_cfg->input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + int weight = wx_font->GetNumericWeight(); + int min_weight = 1, max_weight = 1000; + if (ImGui::SliderInt("##weight", &weight, min_weight, max_weight)) { + wx_font->SetNumericWeight(weight); + m_style_manager.wx_font_changed(); + process(); + } + + wxFont f = wx_font->Bold(); + bool disable = f == *wx_font; + ImGui::SameLine(); + if (draw_button(IconType::bold, disable)) { + *wx_font = f; + m_style_manager.wx_font_changed(); + process(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("wx Make bold").c_str()); + } +#endif // SHOW_WX_WEIGHT_INPUT + + const std::string revert_emboss_depth = _u8L("Revert embossed depth."); + const float *def_depth = exist_stored_style ? + &m_style_manager.get_stored_style()->prop.emboss : nullptr; + bool is_depth_changed = false; + if (use_inch) { + float depthj_in_inch = ObjectManipulation::mm_to_in * font_prop.emboss; + float def_depth_inch = exist_stored_style ? ObjectManipulation::mm_to_in * (*def_depth) : 0.f; + if (def_depth != nullptr) def_depth = &def_depth_inch; + if (rev_input(tr.depth, depthj_in_inch, def_depth, revert_emboss_depth, 0.1f, 0.25, "%.3f in")) { + font_prop.emboss = ObjectManipulation::in_to_mm * depthj_in_inch; + is_depth_changed = true; + } + } else { + if (rev_input(tr.depth, font_prop.emboss, def_depth, revert_emboss_depth, 0.1f, 0.25, "%.2f mm")) + is_depth_changed = true; + } + + if (is_depth_changed) { + Limits::apply(font_prop.emboss, limits.emboss); + process(); + } +} + +bool GLGizmoEmboss::rev_slider(const std::string &name, + std::optional& value, + const std::optional *default_value, + const std::string &undo_tooltip, + int v_min, + int v_max, + const std::string& format, + const wxString &tooltip) +{ + auto draw_slider_optional_int = [&]() -> bool { + float slider_offset = m_gui_cfg->advanced_input_offset; + float slider_width = m_gui_cfg->input_width; + ImGui::SameLine(slider_offset); + ImGui::SetNextItemWidth(slider_width); + return m_imgui->slider_optional_int( ("##" + name).c_str(), value, + v_min, v_max, format.c_str(), 1.f, false, tooltip); + }; + float undo_offset = ImGui::GetStyle().FramePadding.x; + return revertible(name, value, default_value, + undo_tooltip, undo_offset, draw_slider_optional_int); +} + +bool GLGizmoEmboss::rev_slider(const std::string &name, + std::optional& value, + const std::optional *default_value, + const std::string &undo_tooltip, + float v_min, + float v_max, + const std::string& format, + const wxString &tooltip) +{ + auto draw_slider_optional_float = [&]() -> bool { + float slider_offset = m_gui_cfg->advanced_input_offset; + float slider_width = m_gui_cfg->input_width; + ImGui::SameLine(slider_offset); + ImGui::SetNextItemWidth(slider_width); + return m_imgui->slider_optional_float(("##" + name).c_str(), value, + v_min, v_max, format.c_str(), 1.f, false, tooltip); + }; + float undo_offset = ImGui::GetStyle().FramePadding.x; + return revertible(name, value, default_value, + undo_tooltip, undo_offset, draw_slider_optional_float); +} + +bool GLGizmoEmboss::rev_slider(const std::string &name, + float &value, + const float *default_value, + const std::string &undo_tooltip, + float v_min, + float v_max, + const std::string &format, + const wxString &tooltip) +{ + auto draw_slider_float = [&]() -> bool { + float slider_offset = m_gui_cfg->advanced_input_offset; + float slider_width = m_gui_cfg->input_width; + ImGui::SameLine(slider_offset); + ImGui::SetNextItemWidth(slider_width); + return m_imgui->slider_float("##" + name, &value, v_min, v_max, + format.c_str(), 1.f, false, tooltip); + }; + float undo_offset = ImGui::GetStyle().FramePadding.x; + return revertible(name, value, default_value, + undo_tooltip, undo_offset, draw_slider_float); +} + +void GLGizmoEmboss::do_translate(const Vec3d &relative_move) +{ + assert(m_volume != nullptr); + assert(m_volume->text_configuration.has_value()); + Selection &selection = m_parent.get_selection(); + assert(!selection.is_empty()); + selection.setup_cache(); + selection.translate(relative_move, TransformationType::Local); + + std::string snapshot_name; // empty meand no store undo / redo + // NOTE: it use L instead of _L macro because prefix _ is appended inside + // function do_move + // snapshot_name = L("Set surface distance"); + m_parent.do_move(snapshot_name); +} + +void GLGizmoEmboss::do_rotate(float relative_z_angle) +{ + assert(m_volume != nullptr); + assert(m_volume->text_configuration.has_value()); + Selection &selection = m_parent.get_selection(); + assert(!selection.is_empty()); + selection.setup_cache(); + TransformationType transformation_type = TransformationType::Local_Relative_Joint; + selection.rotate(Vec3d(0., 0., relative_z_angle), transformation_type); + + std::string snapshot_name; // empty meand no store undo / redo + // NOTE: it use L instead of _L macro because prefix _ is appended + // inside function do_move + // snapshot_name = L("Set text rotation"); + m_parent.do_rotate(snapshot_name); +} + +void GLGizmoEmboss::set_fine_position() +{ + const Selection &selection = m_parent.get_selection(); + const Selection::IndicesList indices = selection.get_volume_idxs(); + // no selected volume + if (indices.empty()) return; + const GLVolume *volume = selection.get_volume(*indices.begin()); + // bad volume selected (e.g. deleted one) + if (volume == nullptr) return; + + const Camera &camera = wxGetApp().plater()->get_camera(); + Polygon hull = CameraUtils::create_hull2d(camera, *volume); + + const ImVec2 &windows_size = get_minimal_window_size(); + Size c_size = m_parent.get_canvas_size(); + ImVec2 canvas_size(c_size.get_width(), c_size.get_height()); + ImVec2 offset = ImGuiWrapper::suggest_location(windows_size, hull, canvas_size); + m_set_window_offset = offset; + return; + + Polygon rect({Point(offset.x, offset.y), + Point(offset.x + windows_size.x, offset.y), + Point(offset.x + windows_size.x, offset.y + windows_size.y), + Point(offset.x, offset.y + windows_size.y)}); + ImGuiWrapper::draw(hull); + ImGuiWrapper::draw(rect); +} + +void GLGizmoEmboss::draw_advanced() +{ + const auto &ff = m_style_manager.get_font_file_with_cache(); + if (!ff.has_value()) { + ImGui::Text("%s", _u8L("Advanced font options could be change only for corect font.\nStart with select correct font.").c_str()); + return; + } + + FontProp &font_prop = m_style_manager.get_style().prop; + const auto &cn = m_style_manager.get_font_prop().collection_number; + unsigned int font_index = (cn.has_value()) ? *cn : 0; + const auto &font_info = ff.font_file->infos[font_index]; + +#ifdef SHOW_FONT_FILE_PROPERTY + ImGui::SameLine(); + int cache_size = ff.has_value()? (int)ff.cache->size() : 0; + std::string ff_property = + "ascent=" + std::to_string(font_info.ascent) + + ", descent=" + std::to_string(font_info.descent) + + ", lineGap=" + std::to_string(font_info.linegap) + + ", unitPerEm=" + std::to_string(font_info.unit_per_em) + + ", cache(" + std::to_string(cache_size) + " glyphs)"; + if (font_file->infos.size() > 1) { + unsigned int collection = font_prop.collection_number.has_value() ? + *font_prop.collection_number : 0; + ff_property += ", collect=" + std::to_string(collection+1) + "/" + std::to_string(font_file->infos.size()); + } + m_imgui->text_colored(ImGuiWrapper::COL_GREY_DARK, ff_property); +#endif // SHOW_FONT_FILE_PROPERTY + + bool exist_change = false; + auto &tr = m_gui_cfg->translations; + + const EmbossStyle *stored_style = nullptr; + if (m_style_manager.exist_stored_style()) + stored_style = m_style_manager.get_stored_style(); + + bool can_use_surface = (m_volume==nullptr)? false : + (font_prop.use_surface)? true : // already used surface must have option to uncheck + (m_volume->get_object()->volumes.size() > 1); + m_imgui->disabled_begin(!can_use_surface); + const bool *def_use_surface = stored_style ? + &stored_style->prop.use_surface : nullptr; + if (rev_checkbox(tr.use_surface, font_prop.use_surface, def_use_surface, + _u8L("Revert using of model surface."))) { + if (font_prop.use_surface) { + font_prop.distance.reset(); + if (font_prop.emboss < 0.1) + font_prop.emboss = 1; + } + process(); + } + m_imgui->disabled_end(); // !can_use_surface + + std::string units = _u8L("font points"); + std::string units_fmt = "%.0f " + units; + + // input gap between letters + auto def_char_gap = stored_style ? + &stored_style->prop.char_gap : nullptr; + + int half_ascent = font_info.ascent / 2; + int min_char_gap = -half_ascent, max_char_gap = half_ascent; + if (rev_slider(tr.char_gap, font_prop.char_gap, def_char_gap, _u8L("Revert gap between letters"), + min_char_gap, max_char_gap, units_fmt, _L("Distance between letters"))){ + // Condition prevent recalculation when insertint out of limits value by imgui input + if (!Limits::apply(font_prop.char_gap, limits.char_gap) || + !m_volume->text_configuration->style.prop.char_gap.has_value() || + m_volume->text_configuration->style.prop.char_gap != font_prop.char_gap) { + // char gap is stored inside of imgui font atlas + m_style_manager.clear_imgui_font(); + exist_change = true; + } + } + + // input gap between lines + auto def_line_gap = stored_style ? + &stored_style->prop.line_gap : nullptr; + int min_line_gap = -half_ascent, max_line_gap = half_ascent; + if (rev_slider(tr.line_gap, font_prop.line_gap, def_line_gap, _u8L("Revert gap between lines"), + min_line_gap, max_line_gap, units_fmt, _L("Distance between lines"))){ + // Condition prevent recalculation when insertint out of limits value by imgui input + if (!Limits::apply(font_prop.line_gap, limits.line_gap) || + !m_volume->text_configuration->style.prop.line_gap.has_value() || + m_volume->text_configuration->style.prop.line_gap != font_prop.line_gap) { + // line gap is planed to be stored inside of imgui font atlas + m_style_manager.clear_imgui_font(); + exist_change = true; + } + } + + // input boldness + auto def_boldness = stored_style ? + &stored_style->prop.boldness : nullptr; + if (rev_slider(tr.boldness, font_prop.boldness, def_boldness, _u8L("Undo boldness"), + limits.boldness.gui.min, limits.boldness.gui.max, units_fmt, _L("Tiny / Wide glyphs"))){ + if (!Limits::apply(font_prop.boldness, limits.boldness.values) || + !m_volume->text_configuration->style.prop.boldness.has_value() || + m_volume->text_configuration->style.prop.boldness != font_prop.boldness) + exist_change = true; + } + + // input italic + auto def_skew = stored_style ? + &stored_style->prop.skew : nullptr; + if (rev_slider(tr.italic, font_prop.skew, def_skew, _u8L("Undo letter's skew"), + limits.skew.gui.min, limits.skew.gui.max, "%.2f", _L("Italic strength ratio"))){ + if (!Limits::apply(font_prop.skew, limits.skew.values) || + !m_volume->text_configuration->style.prop.skew.has_value() || + m_volume->text_configuration->style.prop.skew != font_prop.skew) + exist_change = true; + } + + // input surface distance + bool allowe_surface_distance = + !m_volume->text_configuration->style.prop.use_surface && + !is_text_object(m_volume); + std::optional &distance = font_prop.distance; + float prev_distance = distance.has_value() ? *distance : .0f, + min_distance = -2 * font_prop.emboss, + max_distance = 2 * font_prop.emboss; + auto def_distance = stored_style ? + &stored_style->prop.distance : nullptr; + m_imgui->disabled_begin(!allowe_surface_distance); + + bool use_inch = wxGetApp().app_config->get("use_inches") == "1"; + const std::string undo_move_tooltip = _u8L("Undo translation"); + const wxString move_tooltip = _L("Distance center of text from model surface"); + bool is_moved = false; + if (use_inch) { + std::optional distance_inch; + if (distance.has_value()) distance_inch = (*distance * ObjectManipulation::mm_to_in); + std::optional def_distance_inch; + if (def_distance != nullptr) { + if (def_distance->has_value()) def_distance_inch = ObjectManipulation::mm_to_in * (*(*def_distance)); + def_distance = &def_distance_inch; + } + min_distance *= ObjectManipulation::mm_to_in; + max_distance *= ObjectManipulation::mm_to_in; + if (rev_slider(tr.surface_distance, distance_inch, def_distance, undo_move_tooltip, min_distance, max_distance, "%.3f in", move_tooltip)) { + if (distance_inch.has_value()) { + font_prop.distance = *distance_inch * ObjectManipulation::in_to_mm; + } else { + font_prop.distance.reset(); + } + is_moved = true; + } + } else { + if (rev_slider(tr.surface_distance, distance, def_distance, undo_move_tooltip, + min_distance, max_distance, "%.2f mm", move_tooltip)) is_moved = true; + } + + if (is_moved){ + m_volume->text_configuration->style.prop.distance = font_prop.distance; + float act_distance = font_prop.distance.has_value() ? *font_prop.distance : .0f; + do_translate(Vec3d::UnitZ() * (act_distance - prev_distance)); + } + m_imgui->disabled_end(); + + // slider for Clock-wise angle in degress + // stored angle is optional CCW and in radians + std::optional &angle = font_prop.angle; + float prev_angle = angle.has_value() ? *angle : .0f; + // Convert stored value to degress + // minus create clock-wise roation from CCW + float angle_deg = angle.has_value() ? + static_cast(-(*angle) * 180 / M_PI) : .0f; + float def_angle_deg_val = + (!stored_style || !stored_style->prop.angle.has_value()) ? + 0.f : (*stored_style->prop.angle * -180 / M_PI); + float* def_angle_deg = stored_style ? + &def_angle_deg_val : nullptr; + if (rev_slider(tr.angle, angle_deg, def_angle_deg, _u8L("Undo rotation"), + limits.angle.min, limits.angle.max, u8"%.2f °", + _L("Rotate text Clock-wise."))) { + // convert back to radians and CCW + angle = -angle_deg * M_PI / 180.0; + to_range_pi_pi(*angle); + if (is_approx(*angle, 0.f)) + angle.reset(); + + m_volume->text_configuration->style.prop.angle = angle; + float act_angle = angle.has_value() ? *angle : .0f; + do_rotate(act_angle - prev_angle); + // recalculate for surface cut + if (font_prop.use_surface) process(); + } + + // when more collection add selector + if (ff.font_file->infos.size() > 1) { + ImGui::Text("%s", tr.collection.c_str()); + ImGui::SameLine(m_gui_cfg->advanced_input_offset); + ImGui::SetNextItemWidth(m_gui_cfg->input_width); + unsigned int selected = font_prop.collection_number.has_value() ? + *font_prop.collection_number : 0; + if (ImGui::BeginCombo("## Font collection", std::to_string(selected).c_str())) { + for (unsigned int i = 0; i < ff.font_file->infos.size(); ++i) { + ImGui::PushID(1 << (10 + i)); + bool is_selected = (i == selected); + if (ImGui::Selectable(std::to_string(i).c_str(), is_selected)) { + if (i == 0) font_prop.collection_number.reset(); + else font_prop.collection_number = i; + exist_change = true; + } + ImGui::PopID(); + } + ImGui::EndCombo(); + } else if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", _u8L("Select from True Type Collection.").c_str()); + } + } + + if (exist_change) { + m_style_manager.clear_glyphs_cache(); + process(); + } +#ifdef ALLOW_DEBUG_MODE + ImGui::Text("family = %s", (font_prop.family.has_value() ? + font_prop.family->c_str() : + " --- ")); + ImGui::Text("face name = %s", (font_prop.face_name.has_value() ? + font_prop.face_name->c_str() : + " --- ")); + ImGui::Text("style = %s", + (font_prop.style.has_value() ? font_prop.style->c_str() : + " --- ")); + ImGui::Text("weight = %s", (font_prop.weight.has_value() ? + font_prop.weight->c_str() : + " --- ")); + + std::string descriptor = style.path; + ImGui::Text("descriptor = %s", descriptor.c_str()); +#endif // ALLOW_DEBUG_MODE +} + +void GLGizmoEmboss::set_minimal_window_size(bool is_advance_edit_style) +{ + ImVec2 window_size = ImGui::GetWindowSize(); + const ImVec2& min_win_size_prev = get_minimal_window_size(); + //ImVec2 diff(window_size.x - min_win_size_prev.x, + // window_size.y - min_win_size_prev.y); + float diff_y = window_size.y - min_win_size_prev.y; + m_is_advanced_edit_style = is_advance_edit_style; + const ImVec2 &min_win_size = get_minimal_window_size(); + ImGui::SetWindowSize(ImVec2(0.f, min_win_size.y + diff_y), + ImGuiCond_Always); +} + +const ImVec2 &GLGizmoEmboss::get_minimal_window_size() const +{ + return (!m_is_advanced_edit_style) ? m_gui_cfg->minimal_window_size : + ((!m_style_manager.has_collections())? m_gui_cfg->minimal_window_size_with_advance : + m_gui_cfg->minimal_window_size_with_collections); +} + +#ifdef ALLOW_ADD_FONT_BY_OS_SELECTOR +bool GLGizmoEmboss::choose_font_by_wxdialog() +{ + wxFontData data; + data.EnableEffects(false); + data.RestrictSelection(wxFONTRESTRICT_SCALABLE); + // set previous selected font + EmbossStyle &selected_style = m_style_manager.get_style(); + if (selected_style.type == WxFontUtils::get_actual_type()) { + std::optional selected_font = WxFontUtils::load_wxFont( + selected_style.path); + if (selected_font.has_value()) data.SetInitialFont(*selected_font); + } + + wxFontDialog font_dialog(wxGetApp().mainframe, data); + if (font_dialog.ShowModal() != wxID_OK) return false; + + data = font_dialog.GetFontData(); + wxFont wx_font = data.GetChosenFont(); + size_t font_index = m_style_manager.get_fonts().size(); + EmbossStyle emboss_style = WxFontUtils::create_emboss_style(wx_font); + + // Check that deserialization NOT influence font + // false - use direct selected wxFont in dialog + // true - use font item (serialize and deserialize wxFont) + bool use_deserialized_font = false; + + // Try load and use new added font + if ((use_deserialized_font && !m_style_manager.load_style(font_index)) || + (!use_deserialized_font && !m_style_manager.load_style(emboss_style, wx_font))) { + m_style_manager.erase(font_index); + wxString message = GUI::format_wxstr( + _L("Font '%1%' can't be used. Please select another."), + emboss_style.name); + wxString title = _L("Selected font is NOT True-type."); + MessageDialog not_loaded_font_message(nullptr, message, title, wxOK); + not_loaded_font_message.ShowModal(); + return choose_font_by_wxdialog(); + } + + // fix dynamic creation of italic font + const auto& cn = m_style_manager.get_font_prop().collection_number; + unsigned int font_collection = cn.has_value() ? *cn : 0; + const auto&ff = m_style_manager.get_font_file_with_cache(); + if (WxFontUtils::is_italic(wx_font) && + !Emboss::is_italic(*ff.font_file, font_collection)) { + m_style_manager.get_style().prop.skew = 0.2; + } + return true; +} +#endif // ALLOW_ADD_FONT_BY_OS_SELECTOR + +#ifdef ALLOW_ADD_FONT_BY_FILE +bool GLGizmoEmboss::choose_true_type_file() +{ + wxArrayString input_files; + wxString fontDir = wxEmptyString; + wxString selectedFile = wxEmptyString; + wxFileDialog dialog(nullptr, _L("Choose one or more files (TTF, TTC):"), + fontDir, selectedFile, file_wildcards(FT_FONTS), + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() == wxID_OK) dialog.GetPaths(input_files); + if (input_files.IsEmpty()) return false; + size_t index = m_style_manager.get_fonts().size(); + // use first valid font + for (auto &input_file : input_files) { + std::string path = std::string(input_file.c_str()); + std::string name = get_file_name(path); + //make_unique_name(name, m_font_list); + const FontProp& prop = m_style_manager.get_font_prop(); + EmbossStyle style{ name, path, EmbossStyle::Type::file_path, prop }; + m_style_manager.add_font(style); + // set first valid added font as active + if (m_style_manager.load_style(index)) return true; + m_style_manager.erase(index); + } + return false; +} +#endif // ALLOW_ADD_FONT_BY_FILE + +bool GLGizmoEmboss::choose_svg_file() +{ + wxArrayString input_files; + wxString fontDir = wxEmptyString; + wxString selectedFile = wxEmptyString; + wxFileDialog dialog(nullptr, _L("Choose SVG file:"), fontDir, + selectedFile, file_wildcards(FT_SVG), + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() == wxID_OK) dialog.GetPaths(input_files); + if (input_files.IsEmpty()) return false; + if (input_files.size() != 1) return false; + auto & input_file = input_files.front(); + std::string path = std::string(input_file.c_str()); + std::string name = get_file_name(path); + + NSVGimage *image = nsvgParseFromFile(path.c_str(), "mm", 96.0f); + ExPolygons polys = NSVGUtils::to_ExPolygons(image); + nsvgDelete(image); + + BoundingBox bb; + for (const auto &p : polys) bb.merge(p.contour.points); + const FontProp &fp = m_style_manager.get_style().prop; + float scale = fp.size_in_mm / std::max(bb.max.x(), bb.max.y()); + auto project = std::make_unique( + std::make_unique(fp.emboss / scale), scale); + indexed_triangle_set its = polygons2model(polys, *project); + return false; + // test store: + // for (auto &poly : polys) poly.scale(1e5); + // SVG svg("converted.svg", BoundingBox(polys.front().contour.points)); + // svg.draw(polys); + //return add_volume(name, its); +} + +bool GLGizmoEmboss::load_configuration(ModelVolume *volume) +{ + if (volume == nullptr) return false; + const std::optional tc_opt = volume->text_configuration; + if (!tc_opt.has_value()) return false; + const TextConfiguration &tc = *tc_opt; + const EmbossStyle &style = tc.style; + + auto has_same_name = [&style](const StyleManager::Item &style_item) -> bool { + const EmbossStyle &es = style_item.style; + return es.name == style.name; + }; + + wxFont wx_font; + bool is_path_changed = false; + if (style.type == WxFontUtils::get_actual_type()) + wx_font = WxFontUtils::load_wxFont(style.path); + if (!wx_font.IsOk()) { + create_notification_not_valid_font(tc); + // Try create similar wx font + wx_font = WxFontUtils::create_wxFont(style); + is_path_changed = wx_font.IsOk(); + } + + const auto& styles = m_style_manager.get_styles(); + auto it = std::find_if(styles.begin(), styles.end(), has_same_name); + if (it == styles.end()) { + // style was not found + if (wx_font.IsOk()) + m_style_manager.load_style(style, wx_font); + } else { + size_t style_index = it - styles.begin(); + if (!m_style_manager.load_style(style_index)) { + // can`t load stored style + m_style_manager.erase(style_index); + if (wx_font.IsOk()) + m_style_manager.load_style(style, wx_font); + + } else { + // stored style is loaded, now set modification of style + m_style_manager.get_style() = style; + m_style_manager.set_wx_font(wx_font); + } + } + + if (is_path_changed) { + std::string path = WxFontUtils::store_wxFont(wx_font); + m_style_manager.get_style().path = path; + } + + m_text = tc.text; + m_volume = volume; + + // store volume state before edit + m_unmodified_volume = {*volume->get_mesh_shared_ptr(), // copy + tc, volume->get_matrix(), volume->name}; + + return true; +} + +void GLGizmoEmboss::create_notification_not_valid_font( + const TextConfiguration &tc) +{ + // not neccessary, but for sure that old notification doesnt exist + if (m_is_unknown_font) remove_notification_not_valid_font(); + m_is_unknown_font = true; + + auto type = NotificationType::UnknownFont; + auto level = + NotificationManager::NotificationLevel::WarningNotificationLevel; + + const EmbossStyle &es = m_style_manager.get_style(); + const auto &origin_family = tc.style.prop.face_name; + const auto &actual_family = es.prop.face_name; + + const std::string &origin_font_name = origin_family.has_value() ? + *origin_family : + tc.style.path; + + std::string actual_wx_face_name; + if (!actual_family.has_value()) { + auto& wx_font = m_style_manager.get_wx_font(); + if (wx_font.has_value()) { + wxString wx_face_name = wx_font->GetFaceName(); + actual_wx_face_name = std::string((const char *) wx_face_name.ToUTF8()); + } + } + + const std::string &actual_font_name = actual_family.has_value() ? *actual_family : + (!actual_wx_face_name.empty() ? actual_wx_face_name : es.path); + + std::string text = + GUI::format(_L("Can't load exactly same font(\"%1%\"), " + "Aplication select similar one(\"%2%\"). " + "You have to specify font for enable edit text."), + origin_font_name, actual_font_name); + auto notification_manager = wxGetApp().plater()->get_notification_manager(); + notification_manager->push_notification(type, level, text); +} + +void GLGizmoEmboss::remove_notification_not_valid_font() +{ + if (!m_is_unknown_font) return; + m_is_unknown_font = false; + auto type = NotificationType::UnknownFont; + auto notification_manager = wxGetApp().plater()->get_notification_manager(); + notification_manager->close_notification_of_type(type); +} + +void GLGizmoEmboss::init_icons() +{ + // icon order has to match the enum IconType + std::vector filenames{ + "edit_button.svg", + "delete.svg", + "add_copies.svg", + "save.svg", + "undo.svg", + "make_italic.svg", + "make_unitalic.svg", + "make_bold.svg", + "make_unbold.svg", + "search.svg", + "open.svg", + "add_text_part.svg", + "add_text_negative.svg", + "add_text_modifier.svg" + }; + assert(filenames.size() == static_cast(IconType::_count)); + std::string path = resources_dir() + "/icons/"; + for (std::string &filename : filenames) filename = path + filename; + + // state order has to match the enum IconState + std::vector> states; + states.push_back(std::make_pair(1, false)); // Activable + states.push_back(std::make_pair(0, false)); // Hovered + states.push_back(std::make_pair(2, false)); // Disabled + + bool compress = false; + bool is_loaded = m_icons_texture.load_from_svg_files_as_sprites_array( + filenames, states, m_gui_cfg->icon_width, compress); + + if (!is_loaded || + (size_t)m_icons_texture.get_width() < (states.size() * m_gui_cfg->icon_width) || + (size_t)m_icons_texture.get_height() < (filenames.size() * m_gui_cfg->icon_width)) { + // bad load of icons, but all usage of m_icons_texture check that texture is initialized + assert(false); + m_icons_texture.reset(); + } +} + +void GLGizmoEmboss::draw_icon(IconType icon, IconState state, ImVec2 size) +{ + // canot draw count + assert(icon != IconType::_count); + if (icon == IconType::_count) return; + + unsigned int icons_texture_id = m_icons_texture.get_id(); + int tex_width = m_icons_texture.get_width(); + int tex_height = m_icons_texture.get_height(); + // is icon loaded + if ((icons_texture_id == 0) || (tex_width <= 1) || (tex_height <= 1)){ + ImGui::Text("▮"); + return; + } + + int icon_width = m_gui_cfg->icon_width; + ImTextureID tex_id = (void *) (intptr_t) (GLuint) icons_texture_id; + int start_x = static_cast(state) * (icon_width + 1) + 1, + start_y = static_cast(icon) * (icon_width + 1) + 1; + + ImVec2 uv0(start_x / (float) tex_width, + start_y / (float) tex_height); + ImVec2 uv1((start_x + icon_width) / (float) tex_width, + (start_y + icon_width) / (float) tex_height); + + if (size.x < 1 || size.y < 1) + size = ImVec2(m_gui_cfg->icon_width, m_gui_cfg->icon_width); + + ImGui::Image(tex_id, size, uv0, uv1); +} + +void GLGizmoEmboss::draw_transparent_icon() +{ + unsigned int icons_texture_id = m_icons_texture.get_id(); + int tex_width = m_icons_texture.get_width(); + int tex_height = m_icons_texture.get_height(); + // is icon loaded + if ((icons_texture_id == 0) || (tex_width <= 1) || (tex_height <= 1)) { + ImGui::Text("▯"); + return; + } + + ImTextureID tex_id = (void *) (intptr_t) (GLuint) icons_texture_id; + int icon_width = m_gui_cfg->icon_width; + ImVec2 icon_size(icon_width, icon_width); + ImVec2 pixel_size(1.f / tex_width, 1.f / tex_height); + // zero pixel is transparent in texture + ImGui::Image(tex_id, icon_size, ImVec2(0, 0), pixel_size); +} + +bool GLGizmoEmboss::draw_clickable( + IconType icon, IconState state, + IconType hover_icon, IconState hover_state) +{ + // check of hover + float cursor_x = ImGui::GetCursorPosX(); + draw_transparent_icon(); + ImGui::SameLine(cursor_x); + + if (ImGui::IsItemHovered()) { + // redraw image + draw_icon(hover_icon, hover_state); + } else { + // redraw normal image + draw_icon(icon, state); + } + return ImGui::IsItemClicked(); +} + +bool GLGizmoEmboss::draw_button(IconType icon, bool disable) +{ + if (disable) { + draw_icon(icon, IconState::disabled); + return false; + } + return draw_clickable( + icon, IconState::activable, + icon, IconState::hovered + ); +} + +bool GLGizmoEmboss::is_text_object(const ModelVolume *text) { + if (text == nullptr) return false; + if (!text->text_configuration.has_value()) return false; + if (text->type() != ModelVolumeType::MODEL_PART) return false; + for (const ModelVolume *v : text->get_object()->volumes) { + if (v == text) continue; + if (v->type() == ModelVolumeType::MODEL_PART) return false; + } + return true; +} + +std::string GLGizmoEmboss::get_file_name(const std::string &file_path) +{ + size_t pos_last_delimiter = file_path.find_last_of("/\\"); + size_t pos_point = file_path.find_last_of('.'); + size_t offset = pos_last_delimiter + 1; + size_t count = pos_point - pos_last_delimiter - 1; + return file_path.substr(offset, count); +} + +///////////// +// priv namespace implementation +/////////////// + +DataBase priv::create_emboss_data_base(const std::string &text, StyleManager& style_manager) +{ + auto create_volume_name = [&]() { + bool contain_enter = text.find('\n') != std::string::npos; + std::string text_fixed; + if (contain_enter) { + // change enters to space + text_fixed = text; // copy + std::replace(text_fixed.begin(), text_fixed.end(), '\n', ' '); + } + return _u8L("Text") + " - " + ((contain_enter) ? text_fixed : text); + }; + + auto create_configuration = [&]() -> TextConfiguration { + if (!style_manager.is_activ_font()) { + std::string default_text_for_emboss = _u8L("Embossed text"); + EmbossStyle es = style_manager.get_style(); + TextConfiguration tc{es, default_text_for_emboss}; + // TODO: investigate how to initialize + return tc; + } + + EmbossStyle &es = style_manager.get_style(); + // actualize font path - during changes in gui it could be corrupted + // volume must store valid path + assert(style_manager.get_wx_font().has_value()); + assert(es.path.compare(WxFontUtils::store_wxFont(*style_manager.get_wx_font())) == 0); + // style.path = WxFontUtils::store_wxFont(*m_style_manager.get_wx_font()); + return TextConfiguration{es, text}; + }; + + return Slic3r::GUI::Emboss::DataBase{style_manager.get_font_file_with_cache(), create_configuration(), create_volume_name()}; +} + + +Transform3d priv::create_transformation_on_bed(const Vec2d &screen_coor, const Camera &camera, const std::vector &bed_shape, double z) +{ + // Create new object + // calculate X,Y offset position for lay on platter in place of + // mouse click + Vec2d bed_coor = CameraUtils::get_z0_position(camera, screen_coor); + + // check point is on build plate: + Points bed_shape_; + bed_shape_.reserve(bed_shape.size()); + for (const Vec2d &p : bed_shape) bed_shape_.emplace_back(p.cast()); + Slic3r::Polygon bed(bed_shape_); + if (!bed.contains(bed_coor.cast())) + // mouse pose is out of build plate so create object in center of plate + bed_coor = bed.centroid().cast(); + + Vec3d offset(bed_coor.x(), bed_coor.y(), z); + // offset -= m_result.center(); + Transform3d::TranslationType tt(offset.x(), offset.y(), offset.z()); + return Transform3d(tt); +} + +void priv::start_create_object_job(DataBase &emboss_data, const Vec2d &coor) +{ + // start creation of new object + Plater *plater = wxGetApp().plater(); + const Camera &camera = plater->get_camera(); + const Pointfs &bed_shape = plater->build_volume().bed_shape(); + + // can't create new object with distance from surface + FontProp &prop = emboss_data.text_configuration.style.prop; + if (prop.distance.has_value()) prop.distance.reset(); + + // can't create new object with using surface + if (prop.use_surface) { + priv::message_disable_cut_surface(); + prop.use_surface = false; + } + + // Transform3d volume_tr = priv::create_transformation_on_bed(mouse_pos, camera, bed_shape, prop.emboss / 2); + DataCreateObject data{std::move(emboss_data), coor, camera, bed_shape}; + auto job = std::make_unique(std::move(data)); + Worker &worker = plater->get_ui_job_worker(); + queue_job(worker, std::move(job)); +} + +void priv::start_create_volume_job(const ModelObject *object, + const Transform3d volume_trmat, + DataBase &emboss_data, + ModelVolumeType volume_type) +{ + bool &use_surface = emboss_data.text_configuration.style.prop.use_surface; + std::unique_ptr job; + if (use_surface) { + // Model to cut surface from. + SurfaceVolumeData::ModelSources sources = create_sources(object->volumes); + if (sources.empty()) { + priv::message_disable_cut_surface(); + use_surface = false; + } else { + bool is_outside = volume_type == ModelVolumeType::MODEL_PART; + // check that there is not unexpected volume type + assert(is_outside || volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::PARAMETER_MODIFIER); + CreateSurfaceVolumeData surface_data{std::move(emboss_data), volume_trmat, is_outside, + std::move(sources), volume_type, object->id()}; + job = std::make_unique(std::move(surface_data)); + } + } + if (!use_surface) { + // create volume + DataCreateVolume data{std::move(emboss_data), volume_type, object->id(), volume_trmat}; + job = std::make_unique(std::move(data)); + } + + Plater *plater = wxGetApp().plater(); + Worker &worker = plater->get_ui_job_worker(); + queue_job(worker, std::move(job)); +} + +GLVolume * priv::get_hovered_gl_volume(const GLCanvas3D &canvas) { + int hovered_id_signed = canvas.get_first_hover_volume_idx(); + if (hovered_id_signed < 0) return nullptr; + + size_t hovered_id = static_cast(hovered_id_signed); + const GLVolumePtrs &volumes = canvas.get_volumes().volumes; + if (hovered_id >= volumes.size()) return nullptr; + + return volumes[hovered_id]; +} + +bool priv::start_create_volume_on_surface_job( + DataBase &emboss_data, ModelVolumeType volume_type, const Vec2d &screen_coor, const GLVolume *gl_volume, RaycastManager &raycaster) +{ + if (gl_volume == nullptr) return false; + Plater *plater = wxGetApp().plater(); + const ModelObjectPtrs &objects = plater->model().objects; + + int object_idx = gl_volume->object_idx(); + if (object_idx < 0 || object_idx >= objects.size()) return false; + ModelObject *obj = objects[object_idx]; + size_t vol_id = obj->volumes[gl_volume->volume_idx()]->id().id; + auto cond = RaycastManager::AllowVolumes({vol_id}); + raycaster.actualize(obj, &cond); + + const Camera &camera = plater->get_camera(); + std::optional hit = raycaster.unproject(screen_coor, camera); + + // context menu for add text could be open only by right click on an + // object. After right click, object is selected and object_idx is set + // also hit must exist. But there is options to add text by object list + if (!hit.has_value()) return false; + + Transform3d hit_object_trmat = raycaster.get_transformation(hit->tr_key); + Transform3d hit_instance_trmat = gl_volume->get_instance_transformation().get_matrix(); + + // Create result volume transformation + Transform3d surface_trmat = create_transformation_onto_surface(hit->position, hit->normal); + const FontProp &font_prop = emboss_data.text_configuration.style.prop; + apply_transformation(font_prop, surface_trmat); + Transform3d volume_trmat = hit_instance_trmat.inverse() * hit_object_trmat * surface_trmat; + start_create_volume_job(obj, volume_trmat, emboss_data, volume_type); + return true; +} + +void priv::find_closest_volume(const Selection &selection, + const Vec2d &screen_center, + const Camera &camera, + const ModelObjectPtrs &objects, + Vec2d *closest_center, + const GLVolume **closest_volume) +{ + assert(closest_center != nullptr); + assert(closest_volume != nullptr); + assert(*closest_volume == nullptr); + const Selection::IndicesList &indices = selection.get_volume_idxs(); + assert(!indices.empty()); // no selected volume + if (indices.empty()) return; + + double center_sq_distance = std::numeric_limits::max(); + for (unsigned int id : indices) { + const GLVolume *gl_volume = selection.get_volume(id); + ModelVolume *volume = priv::get_model_volume(gl_volume, objects); + if (!volume->is_model_part()) continue; + Slic3r::Polygon hull = CameraUtils::create_hull2d(camera, *gl_volume); + Vec2d c = hull.centroid().cast(); + Vec2d d = c - screen_center; + bool is_bigger_x = std::fabs(d.x()) > std::fabs(d.y()); + if ((is_bigger_x && d.x() * d.x() > center_sq_distance) || + (!is_bigger_x && d.y() * d.y() > center_sq_distance)) continue; + + double distance = d.squaredNorm(); + if (center_sq_distance < distance) continue; + center_sq_distance = distance; + *closest_center = c; + *closest_volume = gl_volume; + } +} + +// any existing icon filename to not influence GUI +const std::string GLGizmoEmboss::M_ICON_FILENAME = "cut.svg"; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp new file mode 100644 index 0000000000..092041e96e --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.hpp @@ -0,0 +1,360 @@ +#ifndef slic3r_GLGizmoEmboss_hpp_ +#define slic3r_GLGizmoEmboss_hpp_ + +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, +// which overrides our localization "L" macro. +#include "GLGizmoBase.hpp" +#include "GLGizmoRotate.hpp" +#include "slic3r/GUI/GLTexture.hpp" +#include "slic3r/Utils/RaycastManager.hpp" +#include "slic3r/Utils/EmbossStyleManager.hpp" + +#include "admesh/stl.h" // indexed_triangle_set +#include +#include +#include +#include +#include + +#include "libslic3r/Emboss.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/TextConfiguration.hpp" + +#include +#include + +class wxFont; +namespace Slic3r{ + class AppConfig; + class GLVolume; + + enum class ModelVolumeType : int; +} + +namespace Slic3r::GUI { +class MeshRaycaster; +class GLGizmoEmboss : public GLGizmoBase +{ +public: + GLGizmoEmboss(GLCanvas3D& parent); + + /// + /// Create new embossed text volume by type on position of mouse + /// + /// Object part / Negative volume / Modifier + /// Define position of new volume + void create_volume(ModelVolumeType volume_type, const Vec2d &mouse_pos); + + /// + /// Create new text without given position + /// + /// Object part / Negative volume / Modifier + void create_volume(ModelVolumeType volume_type); + +protected: + bool on_init() override; + std::string on_get_name() const override; + void on_render() override; +#if ENABLE_RAYCAST_PICKING + virtual void on_register_raycasters_for_picking() override; + virtual void on_unregister_raycasters_for_picking() override; +#else // !ENABLE_RAYCAST_PICKING + void on_render_for_picking() override; +#endif // ENABLE_RAYCAST_PICKING + void on_render_input_window(float x, float y, float bottom_limit) override; + bool on_is_activable() const override { return true; } + bool on_is_selectable() const override { return false; } + void on_set_state() override; + + void on_set_hover_id() override{ m_rotate_gizmo.set_hover_id(m_hover_id); } + void on_enable_grabber(unsigned int id) override { m_rotate_gizmo.enable_grabber(); } + void on_disable_grabber(unsigned int id) override { m_rotate_gizmo.disable_grabber(); } + void on_start_dragging() override; + void on_stop_dragging() override; + void on_dragging(const UpdateData &data) override; + + /// + /// Rotate by text on dragging rotate grabers + /// + /// Information about mouse + /// Propagete normaly return false. + bool on_mouse(const wxMouseEvent &mouse_event) override; + + bool wants_enter_leave_snapshots() const override { return true; } + std::string get_gizmo_entering_text() const override { return _u8L("Enter emboss gizmo"); } + std::string get_gizmo_leaving_text() const override { return _u8L("Leave emboss gizmo"); } + std::string get_action_snapshot_name() override { return _u8L("Embossing actions"); } +private: + void initialize(); + static EmbossStyles create_default_styles(); + // localized default text + void set_default_text(); + + void check_selection(); + ModelVolume *get_selected_volume(); + // create volume from text - main functionality + bool process(); + void close(); + void discard_and_close(); + void draw_window(); + void draw_text_input(); + void draw_model_type(); + void fix_transformation(const FontProp &from, const FontProp &to); + void draw_style_list(); + void draw_delete_style_button(); + void draw_style_rename_popup(); + void draw_style_rename_button(); + void draw_style_save_button(bool is_modified); + void draw_style_save_as_popup(); + void draw_style_add_button(); + void init_font_name_texture(); + struct FaceName; + void draw_font_preview(FaceName &face, bool is_visible); + void draw_font_list(); + void draw_style_edit(); + bool draw_italic_button(); + bool draw_bold_button(); + void draw_advanced(); + + bool select_facename(const wxString& facename); + void init_face_names(); + + void do_translate(const Vec3d& relative_move); + void do_rotate(float relative_z_angle); + + /// + /// Move window for edit emboss text near to embossed object + /// NOTE: embossed object must be selected + /// + void set_fine_position(); + + /// + /// Reversible input float with option to restor default value + /// TODO: make more general, static and move to ImGuiWrapper + /// + /// True when value changed otherwise FALSE. + bool rev_input(const std::string &name, float &value, const float *default_value, + const std::string &undo_tooltip, float step, float step_fast, const char *format, + ImGuiInputTextFlags flags = 0); + bool rev_checkbox(const std::string &name, bool &value, const bool* default_value, const std::string &undo_tooltip); + bool rev_slider(const std::string &name, std::optional& value, const std::optional *default_value, + const std::string &undo_tooltip, int v_min, int v_max, const std::string &format, const wxString &tooltip); + bool rev_slider(const std::string &name, std::optional& value, const std::optional *default_value, + const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip); + bool rev_slider(const std::string &name, float &value, const float *default_value, + const std::string &undo_tooltip, float v_min, float v_max, const std::string &format, const wxString &tooltip); + template + bool revertible(const std::string &name, T &value, const T *default_value, const std::string &undo_tooltip, float undo_offset, Draw draw); + + void set_minimal_window_size(bool is_advance_edit_style); + const ImVec2 &get_minimal_window_size() const; + + // process mouse event + bool on_mouse_for_rotation(const wxMouseEvent &mouse_event); + bool on_mouse_for_translate(const wxMouseEvent &mouse_event); + + bool choose_font_by_wxdialog(); + bool choose_true_type_file(); + bool choose_svg_file(); + + bool load_configuration(ModelVolume *volume); + + // When open text loaded from .3mf it could be written with unknown font + bool m_is_unknown_font; + void create_notification_not_valid_font(const TextConfiguration& tc); + void remove_notification_not_valid_font(); + + // This configs holds GUI layout size given by translated texts. + // etc. When language changes, GUI is recreated and this class constructed again, + // so the change takes effect. (info by GLGizmoFdmSupports.hpp) + struct GuiCfg + { + // Zero means it is calculated in init function + ImVec2 minimal_window_size = ImVec2(0, 0); + ImVec2 minimal_window_size_with_advance = ImVec2(0, 0); + ImVec2 minimal_window_size_with_collections = ImVec2(0, 0); + float input_width = 0.f; + float delete_pos_x = 0.f; + float max_style_name_width = 0.f; + unsigned int icon_width = 0; + + // maximal width and height of style image + Vec2i max_style_image_size = Vec2i(0, 0); + + float style_offset = 0.f; + float input_offset = 0.f; + float advanced_input_offset = 0.f; + + ImVec2 text_size; + + // maximal size of face name image + Vec2i face_name_size = Vec2i(100, 0); + float face_name_max_width = 100.f; + float face_name_texture_offset_x = 105.f; + + // maximal texture generate jobs running at once + unsigned int max_count_opened_font_files = 10; + + // Only translations needed for calc GUI size + struct Translations + { + std::string type; + std::string style; + std::string font; + std::string size; + std::string depth; + std::string use_surface; + + // advanced + std::string char_gap; + std::string line_gap; + std::string boldness; + std::string italic; + std::string surface_distance; + std::string angle; + std::string collection; + }; + Translations translations; + + GuiCfg() = default; + }; + std::optional m_gui_cfg; + // setted only when wanted to use - not all the time + std::optional m_set_window_offset; + bool m_is_advanced_edit_style = false; + + Emboss::StyleManager m_style_manager; + + struct FaceName{ + wxString wx_name; + std::string name_truncated = ""; + size_t texture_index = 0; + // State for generation of texture + // when start generate create share pointers + std::shared_ptr> cancel = nullptr; + // R/W only on main thread - finalize of job + std::shared_ptr is_created = nullptr; + }; + + // Keep sorted list of loadable face names + struct Facenames + { + // flag to keep need of enumeration fonts from OS + // false .. wants new enumeration check by Hash + // true .. already enumerated(During opened combo box) + bool is_init = false; + + // data of can_load() faces + std::vector faces = {}; + // Sorter set of Non valid face names in OS + std::vector bad = {}; + + // Configuration of font encoding + const wxFontEncoding encoding = wxFontEncoding::wxFONTENCODING_SYSTEM; + + // Identify if preview texture exists + GLuint texture_id = 0; + + // protection for open too much font files together + // Gtk:ERROR:../../../../gtk/gtkiconhelper.c:494:ensure_surface_for_gicon: assertion failed (error == NULL): Failed to load /usr/share/icons/Yaru/48x48/status/image-missing.png: Error opening file /usr/share/icons/Yaru/48x48/status/image-missing.png: Too many open files (g-io-error-quark, 31) + unsigned int count_opened_font_files = 0; + + // Configuration for texture height + const int count_cached_textures = 32; + + // index for new generated texture index(must be lower than count_cached_textures) + size_t texture_index = 0; + + // hash created from enumerated font from OS + // check when new font was installed + size_t hash = 0; + } m_face_names; + static bool store(const Facenames &facenames); + static bool load(Facenames &facenames); + + + // Text to emboss + std::string m_text; + + // actual volume + ModelVolume *m_volume; + + // state of volume when open EmbossGizmo + struct EmbossVolume + { + TriangleMesh tm; + TextConfiguration tc; + Transform3d tr; + std::string name; + }; + std::optional m_unmodified_volume; + + // True when m_text contain character unknown by selected font + bool m_text_contain_unknown_glyph = false; + + // cancel for previous update of volume to cancel finalize part + std::shared_ptr> m_update_job_cancel; + + // Rotation gizmo + GLGizmoRotate m_rotate_gizmo; + // Value is set only when dragging rotation to calculate actual angle + std::optional m_rotate_start_angle; + + // when draging with text object hold screen offset of cursor from object center + std::optional m_dragging_mouse_offset; + + // TODO: it should be accessible by other gizmo too. + // May be move to plater? + RaycastManager m_raycast_manager; + + // Only when drag text object it stores world position + std::optional m_temp_transformation; + + // drawing icons + GLTexture m_icons_texture; + void init_icons(); + enum class IconType : unsigned { + rename = 0, + erase, + add, + save, + undo, + italic, + unitalic, + bold, + unbold, + system_selector, + open_file, + // VolumeType icons + part, + negative, + modifier, + // automatic calc of icon's count + _count + }; + enum class IconState: unsigned { activable = 0, hovered /*1*/, disabled /*2*/}; + void draw_icon(IconType icon, IconState state, ImVec2 size = ImVec2(0,0)); + void draw_transparent_icon(); + bool draw_clickable(IconType icon, IconState state, IconType hover_icon, IconState hover_state); + bool draw_button(IconType icon, bool disable = false); + + // only temporary solution + static const std::string M_ICON_FILENAME; + +public: + /// + /// Check if text is last solid part of object + /// TODO: move to emboss gui utils + /// + /// Model volume of Text + /// True when object otherwise False + static bool is_text_object(const ModelVolume *text); + + // TODO: move to file utils + static std::string get_file_name(const std::string &file_path); +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_GLGizmoEmboss_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index e6c566d974..8c67014b6e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -295,7 +295,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott const float button_width = m_imgui->calc_text_size(m_desc.at("remove_all")).x + m_imgui->scaled(1.f); const float buttons_width = m_imgui->scaled(0.5f); const float minimal_slider_width = m_imgui->scaled(4.f); - const float color_button_width = m_imgui->calc_text_size("").x + m_imgui->scaled(1.75f); + const float color_button_width = m_imgui->scaled(1.75f); const float combo_label_width = std::max(m_imgui->calc_text_size(m_desc.at("first_color")).x, m_imgui->calc_text_size(m_desc.at("second_color")).x) + m_imgui->scaled(1.f); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index 4f9d5290d7..23edd6143f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -220,8 +220,8 @@ void GLGizmoMove3D::on_render() GLModel::Geometry init_data; init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; init_data.color = AXES_COLOR[id]; - init_data.reserve_vertices(2); - init_data.reserve_indices(2); + init_data.vertices.reserve(2); + init_data.indices.reserve(2); // vertices #if ENABLE_WORLD_COORDINATE @@ -484,4 +484,4 @@ void GLGizmoMove3D::calc_selection_box_and_center() #endif // ENABLE_WORLD_COORDINATE } // namespace GUI -} // namespace Slic3r +} // namespace Slic3r \ No newline at end of file diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp index b4414f4019..7a4d82a41c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp @@ -1,88 +1,88 @@ -#ifndef slic3r_GLGizmoMove_hpp_ -#define slic3r_GLGizmoMove_hpp_ - -#include "GLGizmoBase.hpp" - - -namespace Slic3r { -namespace GUI { - -#if ENABLE_WORLD_COORDINATE -class Selection; -#endif // ENABLE_WORLD_COORDINATE - -class GLGizmoMove3D : public GLGizmoBase -{ - static const double Offset; - - Vec3d m_displacement{ Vec3d::Zero() }; -#if ENABLE_WORLD_COORDINATE - Vec3d m_center{ Vec3d::Zero() }; - BoundingBoxf3 m_bounding_box; -#endif // ENABLE_WORLD_COORDINATE - double m_snap_step{ 1.0 }; - Vec3d m_starting_drag_position{ Vec3d::Zero() }; - Vec3d m_starting_box_center{ Vec3d::Zero() }; - Vec3d m_starting_box_bottom_center{ Vec3d::Zero() }; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - struct GrabberConnection - { - GLModel model; - Vec3d old_center{ Vec3d::Zero() }; - }; - std::array m_grabber_connections; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -public: - GLGizmoMove3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); - virtual ~GLGizmoMove3D() = default; - - double get_snap_step(double step) const { return m_snap_step; } - void set_snap_step(double step) { m_snap_step = step; } - - std::string get_tooltip() const override; - - /// - /// Postpone to Grabber for move - /// - /// Keep information about mouse click - /// Return True when use the information otherwise False. - bool on_mouse(const wxMouseEvent &mouse_event) override; - - /// - /// Detect reduction of move for wipetover on selection change - /// - void data_changed() override; -protected: - bool on_init() override; - std::string on_get_name() const override; - bool on_is_activable() const override; - void on_start_dragging() override; - void on_stop_dragging() override; - void on_dragging(const UpdateData& data) override; - void on_render() override; -#if ENABLE_RAYCAST_PICKING - virtual void on_register_raycasters_for_picking() override; - virtual void on_unregister_raycasters_for_picking() override; -#else - void on_render_for_picking() override; -#endif // ENABLE_RAYCAST_PICKING - -private: - double calc_projection(const UpdateData& data) const; -#if ENABLE_WORLD_COORDINATE -#if ENABLE_LEGACY_OPENGL_REMOVAL - Transform3d local_transform(const Selection& selection) const; -#else - void transform_to_local(const Selection& selection) const; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - void calc_selection_box_and_center(); -#endif // ENABLE_WORLD_COORDINATE -}; - - -} // namespace GUI -} // namespace Slic3r - -#endif // slic3r_GLGizmoMove_hpp_ +#ifndef slic3r_GLGizmoMove_hpp_ +#define slic3r_GLGizmoMove_hpp_ + +#include "GLGizmoBase.hpp" + + +namespace Slic3r { +namespace GUI { + +#if ENABLE_WORLD_COORDINATE +class Selection; +#endif // ENABLE_WORLD_COORDINATE + +class GLGizmoMove3D : public GLGizmoBase +{ + static const double Offset; + + Vec3d m_displacement{ Vec3d::Zero() }; +#if ENABLE_WORLD_COORDINATE + Vec3d m_center{ Vec3d::Zero() }; + BoundingBoxf3 m_bounding_box; +#endif // ENABLE_WORLD_COORDINATE + double m_snap_step{ 1.0 }; + Vec3d m_starting_drag_position{ Vec3d::Zero() }; + Vec3d m_starting_box_center{ Vec3d::Zero() }; + Vec3d m_starting_box_bottom_center{ Vec3d::Zero() }; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + struct GrabberConnection + { + GLModel model; + Vec3d old_center{ Vec3d::Zero() }; + }; + std::array m_grabber_connections; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +public: + GLGizmoMove3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + virtual ~GLGizmoMove3D() = default; + + double get_snap_step(double step) const { return m_snap_step; } + void set_snap_step(double step) { m_snap_step = step; } + + std::string get_tooltip() const override; + + /// + /// Postpone to Grabber for move + /// + /// Keep information about mouse click + /// Return True when use the information otherwise False. + bool on_mouse(const wxMouseEvent &mouse_event) override; + + /// + /// Detect reduction of move for wipetover on selection change + /// + void data_changed() override; +protected: + bool on_init() override; + std::string on_get_name() const override; + bool on_is_activable() const override; + void on_start_dragging() override; + void on_stop_dragging() override; + void on_dragging(const UpdateData& data) override; + void on_render() override; +#if ENABLE_RAYCAST_PICKING + virtual void on_register_raycasters_for_picking() override; + virtual void on_unregister_raycasters_for_picking() override; +#else + void on_render_for_picking() override; +#endif // ENABLE_RAYCAST_PICKING + +private: + double calc_projection(const UpdateData& data) const; +#if ENABLE_WORLD_COORDINATE +#if ENABLE_LEGACY_OPENGL_REMOVAL + Transform3d local_transform(const Selection& selection) const; +#else + void transform_to_local(const Selection& selection) const; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + void calc_selection_box_and_center(); +#endif // ENABLE_WORLD_COORDINATE +}; + + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoMove_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index 80d6f1aa05..6c57df7e59 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -287,7 +287,8 @@ void GLGizmoRotate::on_render_for_picking() void GLGizmoRotate::init_data_from_selection(const Selection& selection) { ECoordinatesType coordinates_type; - if (selection.is_wipe_tower()) + if (m_using_local_coordinate || + selection.is_wipe_tower()) coordinates_type = ECoordinatesType::Local; else coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp index 54a40dd595..cbc0f104b3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp @@ -57,6 +57,9 @@ private: float m_old_angle{ 0.0f }; #endif // ENABLE_LEGACY_OPENGL_REMOVAL + // emboss need to draw rotation gizmo in local coordinate systems + bool m_using_local_coordinate{false}; + ColorRGBA m_drag_color; ColorRGBA m_highlight_color; @@ -69,6 +72,9 @@ public: std::string get_tooltip() const override; + void set_group_id(int group_id) { m_group_id = group_id; } + void set_using_local_coordinate(bool use) { m_using_local_coordinate =use;} + void start_dragging(); void stop_dragging(); @@ -230,4 +236,4 @@ private: } // namespace GUI } // namespace Slic3r -#endif // slic3r_GLGizmoRotate_hpp_ +#endif // slic3r_GLGizmoRotate_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp index 4c311a1a80..03be5c2f4c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp @@ -937,4 +937,4 @@ void GLGizmoScale3D::transform_to_local(const Selection& selection) const #endif // ENABLE_WORLD_COORDINATE } // namespace GUI -} // namespace Slic3r +} // namespace Slic3r \ No newline at end of file diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp index b92c54f183..d51cec8150 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp @@ -125,4 +125,4 @@ private: } // namespace GUI } // namespace Slic3r -#endif // slic3r_GLGizmoScale_hpp_ +#endif // slic3r_GLGizmoScale_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index d501e64591..a127369532 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -95,10 +95,8 @@ static std::string create_volumes_name(const std::set& ids, const Sele return name; } -GLGizmoSimplify::GLGizmoSimplify(GLCanvas3D & parent, - const std::string &icon_filename, - unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, -1) +GLGizmoSimplify::GLGizmoSimplify(GLCanvas3D &parent) + : GLGizmoBase(parent, M_ICON_FILENAME, -1) , m_show_wireframe(false) , m_move_to_center(false) , m_original_triangle_count(0) @@ -588,7 +586,7 @@ void GLGizmoSimplify::on_set_state() void GLGizmoSimplify::create_gui_cfg() { if (m_gui_cfg.has_value()) return; - int space_size = m_imgui->calc_text_size(":MM").x; + int space_size = m_imgui->calc_text_size(std::string_view{":MM"}).x; GuiCfg cfg; cfg.top_left_width = std::max(m_imgui->calc_text_size(tr_mesh_name).x, m_imgui->calc_text_size(tr_triangles).x) @@ -844,4 +842,7 @@ void GLGizmoSimplify::Configuration::fix_count_by_ratio(size_t triangle_count) triangle_count * (100.f - decimate_ratio) / 100.f)); } +// any existing icon filename to not influence GUI +const std::string GLGizmoSimplify::M_ICON_FILENAME = "cut.svg"; + } // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp index ae2814ee9d..8317b45d71 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.hpp @@ -20,7 +20,7 @@ class NotificationManager; // for simplify suggestion class GLGizmoSimplify: public GLGizmoBase { public: - GLGizmoSimplify(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + GLGizmoSimplify(GLCanvas3D& parent); virtual ~GLGizmoSimplify(); bool on_esc_key_down(); static void add_simplify_suggestion_notification( @@ -155,6 +155,9 @@ private: return L("Model simplification has been canceled"); } }; + + // only temporary solution + static const std::string M_ICON_FILENAME; }; } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 07b4baa350..37d1d3de4c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -21,6 +21,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMeasure.hpp" #include "libslic3r/format.hpp" @@ -106,8 +107,9 @@ bool GLGizmosManager::init() m_gizmos.emplace_back(new GLGizmoFdmSupports(m_parent, "fdm_supports.svg", 7)); m_gizmos.emplace_back(new GLGizmoSeam(m_parent, "seam.svg", 8)); m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, "mmu_segmentation.svg", 9)); - m_gizmos.emplace_back(new GLGizmoSimplify(m_parent, "cut.svg", 10)); - m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", 11)); + m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, "measure.svg", 10)); + m_gizmos.emplace_back(new GLGizmoEmboss(m_parent)); + m_gizmos.emplace_back(new GLGizmoSimplify(m_parent)); m_common_gizmos_data.reset(new CommonGizmosDataPool(&m_parent)); @@ -1064,9 +1066,12 @@ bool GLGizmosManager::generate_icons_texture() { std::string path = resources_dir() + "/icons/"; std::vector filenames; - for (size_t idx = 0; idxget_icon_filename(); + for (size_t idx=0; idxget_icon_filename(); if (!icon_filename.empty()) filenames.push_back(path + icon_filename); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 1689cf4616..1469c2ef75 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -79,8 +79,9 @@ public: FdmSupports, Seam, MmuSegmentation, - Simplify, Measure, + Emboss, + Simplify, Undefined }; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 01cd8ec30c..17709788d7 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -38,6 +38,9 @@ #include #include +// suggest location +#include "libslic3r/ClipperUtils.hpp" // Slic3r::intersection + namespace Slic3r { namespace GUI { @@ -300,10 +303,27 @@ void ImGuiWrapper::render() m_new_frame_open = false; } -ImVec2 ImGuiWrapper::calc_text_size(const wxString &text, float wrap_width) const +ImVec2 ImGuiWrapper::calc_text_size(std::string_view text, + bool hide_text_after_double_hash, + float wrap_width) +{ + return ImGui::CalcTextSize(text.data(), text.data() + text.length(), + hide_text_after_double_hash, wrap_width); +} + +ImVec2 ImGuiWrapper::calc_text_size(const std::string& text, + bool hide_text_after_double_hash, + float wrap_width) +{ + return ImGui::CalcTextSize(text.c_str(), NULL, hide_text_after_double_hash, wrap_width); +} + +ImVec2 ImGuiWrapper::calc_text_size(const wxString &text, + bool hide_text_after_double_hash, + float wrap_width) { auto text_utf8 = into_u8(text); - ImVec2 size = ImGui::CalcTextSize(text_utf8.c_str(), NULL, false, wrap_width); + ImVec2 size = ImGui::CalcTextSize(text_utf8.c_str(), NULL, hide_text_after_double_hash, wrap_width); /*#ifdef __linux__ size.x *= m_style_scaling; @@ -389,6 +409,18 @@ bool ImGuiWrapper::button(const wxString& label, float width, float height) return ImGui::Button(label_utf8.c_str(), ImVec2(width, height)); } +bool ImGuiWrapper::button(const wxString& label, const ImVec2 &size, bool enable) +{ + disabled_begin(!enable); + + auto label_utf8 = into_u8(label); + bool res = ImGui::Button(label_utf8.c_str(), size); + + disabled_end(); + return (enable) ? res : false; +} + + bool ImGuiWrapper::radio_button(const wxString &label, bool active) { auto label_utf8 = into_u8(label); @@ -442,13 +474,13 @@ void ImGuiWrapper::text(const char *label) void ImGuiWrapper::text(const std::string &label) { - this->text(label.c_str()); + ImGuiWrapper::text(label.c_str()); } void ImGuiWrapper::text(const wxString &label) { auto label_utf8 = into_u8(label); - this->text(label_utf8.c_str()); + ImGuiWrapper::text(label_utf8.c_str()); } void ImGuiWrapper::text_colored(const ImVec4& color, const char* label) @@ -458,13 +490,13 @@ void ImGuiWrapper::text_colored(const ImVec4& color, const char* label) void ImGuiWrapper::text_colored(const ImVec4& color, const std::string& label) { - this->text_colored(color, label.c_str()); + ImGuiWrapper::text_colored(color, label.c_str()); } void ImGuiWrapper::text_colored(const ImVec4& color, const wxString& label) { auto label_utf8 = into_u8(label); - this->text_colored(color, label_utf8.c_str()); + ImGuiWrapper::text_colored(color, label_utf8.c_str()); } void ImGuiWrapper::text_wrapped(const char *label, float wrap_width) @@ -1152,6 +1184,307 @@ ColorRGBA ImGuiWrapper::from_ImVec4(const ImVec4& color) return { color.x, color.y, color.z, color.w }; } +template +static bool input_optional(std::optional &v, Func& f, std::function is_default, const T& def_val) +{ + if (v.has_value()) { + if (f(*v)) { + if (is_default(*v)) v.reset(); + return true; + } + } else { + T val = def_val; + if (f(val)) { + if (!is_default(val)) v = val; + return true; + } + } + return false; +} + +bool ImGuiWrapper::input_optional_int(const char * label, + std::optional& v, + int step, + int step_fast, + ImGuiInputTextFlags flags, + int def_val) +{ + auto func = [&](int &value) { + return ImGui::InputInt(label, &value, step, step_fast, flags); + }; + std::function is_default = + [def_val](const int &value) -> bool { return value == def_val; }; + return input_optional(v, func, is_default, def_val); +} + +bool ImGuiWrapper::input_optional_float(const char * label, + std::optional &v, + float step, + float step_fast, + const char * format, + ImGuiInputTextFlags flags, + float def_val) +{ + auto func = [&](float &value) { + return ImGui::InputFloat(label, &value, step, step_fast, format, flags); + }; + std::function is_default = + [def_val](const float &value) -> bool { + return std::fabs(value-def_val) <= std::numeric_limits::epsilon(); + }; + return input_optional(v, func, is_default, def_val); +} + +bool ImGuiWrapper::drag_optional_float(const char * label, + std::optional &v, + float v_speed, + float v_min, + float v_max, + const char * format, + float power, + float def_val) +{ + auto func = [&](float &value) { + return ImGui::DragFloat(label, &value, v_speed, v_min, v_max, format, power); + }; + std::function is_default = + [def_val](const float &value) -> bool { + return std::fabs(value-def_val) <= std::numeric_limits::epsilon(); + }; + return input_optional(v, func, is_default, def_val); +} + +bool ImGuiWrapper::slider_optional_float(const char *label, + std::optional &v, + float v_min, + float v_max, + const char *format, + float power, + bool clamp, + const wxString &tooltip, + bool show_edit_btn, + float def_val) +{ + auto func = [&](float &value) { + return slider_float(label, &value, v_min, v_max, format, power, clamp, tooltip, show_edit_btn); + }; + std::function is_default = + [def_val](const float &value) -> bool { + return std::fabs(value - def_val) <= std::numeric_limits::epsilon(); + }; + return input_optional(v, func, is_default, def_val); +} + +bool ImGuiWrapper::slider_optional_int(const char *label, + std::optional &v, + int v_min, + int v_max, + const char *format, + float power, + bool clamp, + const wxString &tooltip, + bool show_edit_btn, + int def_val) +{ + std::optional val; + if (v.has_value()) val = static_cast(*v); + auto func = [&](float &value) { + return slider_float(label, &value, v_min, v_max, format, power, clamp, tooltip, show_edit_btn); + }; + std::function is_default = + [def_val](const float &value) -> bool { + return std::fabs(value - def_val) < 0.9f; + }; + + float default_value = static_cast(def_val); + if (input_optional(val, func, is_default, default_value)) { + if (val.has_value()) + v = static_cast(std::round(*val)); + else + v.reset(); + return true; + } else return false; +} + +std::string ImGuiWrapper::trunc(const std::string &text, + float width, + const char * tail) +{ + float text_width = ImGui::CalcTextSize(text.c_str()).x; + if (text_width < width) return text; + float tail_width = ImGui::CalcTextSize(tail).x; + assert(width > tail_width); + if (width <= tail_width) return "Error: Can't add tail and not be under wanted width."; + float allowed_width = width - tail_width; + + // guess approx count of letter + float average_letter_width = calc_text_size(std::string_view("n")).x; // average letter width + unsigned count_letter = static_cast(allowed_width / average_letter_width); + + std::string_view text_ = text; + std::string_view result_text = text_.substr(0, count_letter); + text_width = calc_text_size(result_text).x; + if (text_width < allowed_width) { + // increase letter count + while (count_letter < text.length()) { + ++count_letter; + std::string_view act_text = text_.substr(0, count_letter); + text_width = calc_text_size(act_text).x; + if (text_width > allowed_width) break; + result_text = act_text; + } + } else { + // decrease letter count + while (count_letter > 1) { + --count_letter; + result_text = text_.substr(0, count_letter); + text_width = calc_text_size(result_text).x; + if (text_width < allowed_width) break; + } + } + return std::string(result_text) + tail; +} + +void ImGuiWrapper::escape_double_hash(std::string &text) +{ + // add space between hashes + const std::string search = "##"; + const std::string replace = "# #"; + size_t pos = 0; + while ((pos = text.find(search, pos)) != std::string::npos) + text.replace(pos, search.length(), replace); +} + +ImVec2 ImGuiWrapper::suggest_location(const ImVec2 &dialog_size, + const Slic3r::Polygon &interest, + const ImVec2 &canvas_size) +{ + // IMPROVE 1: do not select place over menu + // BoundingBox top_menu; + // GLGizmosManager &gizmo_mng = canvas->get_gizmos_manager(); + // BoundingBox side_menu; // gizmo_mng.get_size(); + // BoundingBox left_bottom_menu; // is permanent? + // NotificationManager *notify_mng = plater->get_notification_manager(); + // BoundingBox notifications; // notify_mng->get_size(); + // m_window_width, m_window_height + position + + // IMPROVE 2: use polygon of interest not only bounding box + BoundingBox bb(interest.points); + Point center = bb.center(); // interest.centroid(); + + // area size + Point window_center(canvas_size.x / 2, canvas_size.y / 2); + + // mov on side + Point bb_half_size = (bb.max - bb.min) / 2 + Point(1,1); + Point diff_center = window_center - center; + Vec2d diff_norm(diff_center.x() / (double) bb_half_size.x(), + diff_center.y() / (double) bb_half_size.y()); + if (diff_norm.x() > 1.) diff_norm.x() = 1.; + if (diff_norm.x() < -1.) diff_norm.x() = -1.; + if (diff_norm.y() > 1.) diff_norm.y() = 1.; + if (diff_norm.y() < -1.) diff_norm.y() = -1.; + + Vec2d abs_diff(abs(diff_norm.x()), abs(diff_norm.y())); + if (abs_diff.x() < 1. && abs_diff.y() < 1.) { + if (abs_diff.x() > abs_diff.y()) + diff_norm.x() = (diff_norm.x() < 0.) ? (-1.) : 1.; + else + diff_norm.y() = (diff_norm.y() < 0.) ? (-1.) : 1.; + } + + Point half_dialog_size(dialog_size.x / 2., dialog_size.y / 2.); + Point move_size = bb_half_size + half_dialog_size; + Point offseted_center = center - half_dialog_size; + Vec2d offset(offseted_center.x() + diff_norm.x() * move_size.x(), + offseted_center.y() + diff_norm.y() * move_size.y()); + + // move offset close to center + Points window_polygon = {offset.cast(), + Point(offset.x(), offset.y() + dialog_size.y), + Point(offset.x() + dialog_size.x, + offset.y() + dialog_size.y), + Point(offset.x() + dialog_size.x, offset.y())}; + // check that position by Bounding box is not intersecting + assert(Slic3r::intersection(interest, Polygon(window_polygon)).empty()); + + double allowed_space = 10; // in px + double allowed_space_sq = allowed_space * allowed_space; + Vec2d move_vec = (center - (offset.cast() + half_dialog_size)) + .cast(); + Vec2d result_move(0, 0); + do { + move_vec = move_vec / 2.; + Point move_point = (move_vec + result_move).cast(); + Points moved_polygon = window_polygon; // copy + for (Point &p : moved_polygon) p += move_point; + if (Slic3r::intersection(interest, Polygon(moved_polygon)).empty()) + result_move += move_vec; + + } while (move_vec.squaredNorm() >= allowed_space_sq); + offset += result_move; + + return ImVec2(offset.x(), offset.y()); +} + +void ImGuiWrapper::draw( + const Polygon &polygon, + ImDrawList * draw_list /* = ImGui::GetOverlayDrawList()*/, + ImU32 color /* = ImGui::GetColorU32(COL_ORANGE_LIGHT)*/, + float thickness /* = 3.f*/) +{ + // minimal one line consist of 2 points + if (polygon.size() < 2) return; + // need a place to draw + if (draw_list == nullptr) return; + + const Point *prev_point = &polygon.points.back(); + for (const Point &point : polygon.points) { + ImVec2 p1(prev_point->x(), prev_point->y()); + ImVec2 p2(point.x(), point.y()); + draw_list->AddLine(p1, p2, color, thickness); + prev_point = &point; + } +} + +bool ImGuiWrapper::contain_all_glyphs(const ImFont *font, + const std::string &text) +{ + if (font == nullptr) return false; + if (!font->IsLoaded()) return false; + const ImFontConfig *fc = font->ConfigData; + if (fc == nullptr) return false; + if (text.empty()) return true; + return is_chars_in_ranges(fc->GlyphRanges, text.c_str()); +} + +bool ImGuiWrapper::is_char_in_ranges(const ImWchar *ranges, + unsigned int letter) +{ + for (const ImWchar *range = ranges; range[0] && range[1]; range += 2) { + ImWchar from = range[0]; + ImWchar to = range[1]; + if (from <= letter && letter <= to) return true; + if (letter < to) return false; // ranges should be sorted + } + return false; +}; + +bool ImGuiWrapper::is_chars_in_ranges(const ImWchar *ranges, + const char *chars_ptr) +{ + while (*chars_ptr) { + unsigned int c = 0; + // UTF-8 to 32-bit character need imgui_internal + int c_len = ImTextCharFromUtf8(&c, chars_ptr, NULL); + chars_ptr += c_len; + if (c_len == 0) break; + if (!is_char_in_ranges(ranges, c)) return false; + } + return true; +} + + #ifdef __APPLE__ static const ImWchar ranges_keyboard_shortcuts[] = { @@ -1206,7 +1539,7 @@ void ImGuiWrapper::init_font(bool compress) // Create ranges of characters from m_glyph_ranges, possibly adding some OS specific special characters. ImVector ranges; - ImFontAtlas::GlyphRangesBuilder builder; + ImFontGlyphRangesBuilder builder; builder.AddRanges(m_glyph_ranges); if (m_font_cjk) { diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index 040e5e4916..3365954bab 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -2,6 +2,7 @@ #define slic3r_ImGuiWrapper_hpp_ #include +#include #include #include @@ -10,6 +11,7 @@ #include "libslic3r/Point.hpp" #include "libslic3r/Color.hpp" +#include "libslic3r/Polygon.hpp" namespace Slic3r { namespace Search { @@ -52,8 +54,6 @@ public: ImGuiWrapper(); ~ImGuiWrapper(); - void read_glsl_version(); - void set_language(const std::string &language); void set_display_size(float w, float h); void set_scaling(float font_size, float scale_style, float scale_both); @@ -62,13 +62,19 @@ public: float get_font_size() const { return m_font_size; } float get_style_scaling() const { return m_style_scaling; } + const ImWchar *get_glyph_ranges() const { return m_glyph_ranges; } // language specific void new_frame(); void render(); float scaled(float x) const { return x * m_font_size; } ImVec2 scaled(float x, float y) const { return ImVec2(x * m_font_size, y * m_font_size); } - ImVec2 calc_text_size(const wxString &text, float wrap_width = -1.0f) const; + /// + /// Extend ImGui::CalcTextSize to use string_view + /// + static ImVec2 calc_text_size(std::string_view text, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + static ImVec2 calc_text_size(const std::string& text, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + static ImVec2 calc_text_size(const wxString &text, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); ImVec2 calc_button_size(const wxString &text, const ImVec2 &button_size = ImVec2(0, 0)) const; ImVec2 get_item_spacing() const; @@ -87,15 +93,16 @@ public: bool button(const wxString &label); bool button(const wxString& label, float width, float height); + bool button(const wxString& label, const ImVec2 &size, bool enable); // default size = ImVec2(0.f, 0.f) bool radio_button(const wxString &label, bool active); bool draw_radio_button(const std::string& name, float size, bool active, std::function draw_callback); bool checkbox(const wxString &label, bool &value); - void text(const char *label); - void text(const std::string &label); - void text(const wxString &label); - void text_colored(const ImVec4& color, const char* label); - void text_colored(const ImVec4& color, const std::string& label); - void text_colored(const ImVec4& color, const wxString& label); + static void text(const char *label); + static void text(const std::string &label); + static void text(const wxString &label); + static void text_colored(const ImVec4& color, const char* label); + static void text_colored(const ImVec4& color, const std::string& label); + static void text_colored(const ImVec4& color, const wxString& label); void text_wrapped(const char *label, float wrap_width); void text_wrapped(const std::string &label, float wrap_width); void text_wrapped(const wxString &label, float wrap_width); @@ -126,6 +133,77 @@ public: bool want_text_input() const; bool want_any_input() const; + // Optional inputs are used for set up value inside of an optional, with default value + // + // Extended function ImGui::InputInt to work with std::optional, when value == def_val optional is released. + static bool input_optional_int(const char *label, std::optional& v, int step=1, int step_fast=100, ImGuiInputTextFlags flags=0, int def_val = 0); + // Extended function ImGui::InputFloat to work with std::optional value near def_val cause release of optional + static bool input_optional_float(const char* label, std::optional &v, float step = 0.0f, float step_fast = 0.0f, const char* format = "%.3f", ImGuiInputTextFlags flags = 0, float def_val = .0f); + // Extended function ImGui::DragFloat to work with std::optional value near def_val cause release of optional + static bool drag_optional_float(const char* label, std::optional &v, float v_speed, float v_min, float v_max, const char* format, float power, float def_val = .0f); + // Extended function ImGuiWrapper::slider_float to work with std::optional value near def_val cause release of optional + bool slider_optional_float(const char* label, std::optional &v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f, bool clamp = true, const wxString& tooltip = {}, bool show_edit_btn = true, float def_val = .0f); + // Extended function ImGuiWrapper::slider_float to work with std::optional, when value == def_val than optional release its value + bool slider_optional_int(const char* label, std::optional &v, int v_min, int v_max, const char* format = "%.3f", float power = 1.0f, bool clamp = true, const wxString& tooltip = {}, bool show_edit_btn = true, int def_val = 0); + + /// + /// Truncate text by ImGui draw function to specific width + /// NOTE 1: ImGui must be initialized + /// NOTE 2: Calculation for actual acive imgui font + /// + /// Text to be truncated + /// Maximal width before truncate + /// String puted on end of text to be visible truncation + /// Truncated text + static std::string trunc(const std::string &text, + float width, + const char *tail = " .."); + + /// + /// Escape ## in data by add space between hashes + /// Needed when user written text is visualized by ImGui. + /// + /// In/Out text to be escaped + static void escape_double_hash(std::string &text); + + /// + /// Suggest loacation of dialog window, + /// dependent on actual visible thing on platter + /// like Gizmo menu size, notifications, ... + /// To be near of polygon interest and not over it. + /// And also not out of visible area. + /// + /// Define width and height of diaog window + /// Area of interest. Result should be close to it + /// Available space a.k.a GLCanvas3D::get_current_canvas3D() + /// Suggestion for dialog offest + static ImVec2 suggest_location(const ImVec2 &dialog_size, + const Slic3r::Polygon &interest, + const ImVec2 &canvas_size); + + /// + /// Visualization of polygon + /// + /// Define what to draw + /// Define where to draw it + /// Color of polygon + /// Width of polygon line + static void draw(const Polygon &polygon, + ImDrawList * draw_list = ImGui::GetOverlayDrawList(), + ImU32 color = ImGui::GetColorU32(COL_ORANGE_LIGHT), + float thickness = 3.f); + + /// + /// Check that font ranges contain all chars in string + /// (rendered Unicodes are stored in GlyphRanges) + /// + /// Contain glyph ranges + /// Vector of character to check + /// True when all glyphs from text are in font ranges + static bool contain_all_glyphs(const ImFont *font, const std::string &text); + static bool is_chars_in_ranges(const ImWchar *ranges, const char *chars_ptr); + static bool is_char_in_ranges(const ImWchar *ranges, unsigned int letter); + bool requires_extra_frame() const { return m_requires_extra_frame; } void set_requires_extra_frame() { m_requires_extra_frame = true; } void reset_requires_extra_frame() { m_requires_extra_frame = false; } diff --git a/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp new file mode 100644 index 0000000000..753dcdbfd2 --- /dev/null +++ b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.cpp @@ -0,0 +1,144 @@ +#include "CreateFontNameImageJob.hpp" + +#include "libslic3r/Emboss.hpp" +// rasterization of ExPoly +#include "libslic3r/SLA/AGGRaster.hpp" + +#include "slic3r/Utils/WxFontUtils.hpp" +#include "slic3r/GUI/3DScene.hpp" // ::glsafe + +// ability to request new frame after finish rendering +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" + +#include "wx/fontenum.h" + +#include + +using namespace Slic3r; +using namespace Slic3r::GUI; + +CreateFontImageJob::CreateFontImageJob(FontImageData &&input) + : m_input(std::move(input)) +{ + assert(!m_input.text.empty()); + assert(wxFontEnumerator::IsValidFacename(m_input.font_name)); + assert(m_input.gray_level > 0 && m_input.gray_level < 255); + assert(m_input.texture_id != 0); +} + +void CreateFontImageJob::process(Ctl &ctl) +{ + if (!wxFontEnumerator::IsValidFacename(m_input.font_name)) return; + // Select font + wxFont wx_font( + wxFontInfo().FaceName(m_input.font_name).Encoding(m_input.encoding)); + if (!wx_font.IsOk()) return; + + std::unique_ptr font_file = + WxFontUtils::create_font_file(wx_font); + if (font_file == nullptr) return; + + Emboss::FontFileWithCache font_file_with_cache(std::move(font_file)); + FontProp fp; + // use only first line of text + std::string text = m_input.text; + size_t enter_pos = text.find('\n'); + if (enter_pos < text.size()) { + // text start with enter + if (enter_pos == 0) return; + // exist enter, soo delete all after enter + text = text.substr(0, enter_pos); + } + + std::function was_canceled = [&ctl, cancel = m_input.cancel]() -> bool { + if (ctl.was_canceled()) return true; + if (cancel->load()) return true; + return false; + }; + ExPolygons shapes = Emboss::text2shapes(font_file_with_cache, text.c_str(), fp, was_canceled); + // normalize height of font + BoundingBox bounding_box; + for (ExPolygon &shape : shapes) + bounding_box.merge(BoundingBox(shape.contour.points)); + if (bounding_box.size().x() < 1 || bounding_box.size().y() < 1) return; + double scale = m_input.size.y() / (double) bounding_box.size().y(); + BoundingBoxf bb2(bounding_box.min.cast(), + bounding_box.max.cast()); + bb2.scale(scale); + Vec2d size_f = bb2.size(); + m_tex_size = Point(std::ceil(size_f.x()), std::ceil(size_f.y())); + // crop image width + if (m_tex_size.x() > m_input.size.x()) m_tex_size.x() = m_input.size.x(); + if (m_tex_size.y() > m_input.size.y()) m_tex_size.y() = m_input.size.y(); + + // Set up result + m_result = std::vector(m_tex_size.x() * m_tex_size.y() * 4, {255}); + + sla::Resolution resolution(m_tex_size.x(), m_tex_size.y()); + double pixel_dim = SCALING_FACTOR / scale; + sla::PixelDim dim(pixel_dim, pixel_dim); + double gamma = 1.; + std::unique_ptr r = + sla::create_raster_grayscale_aa(resolution, dim, gamma); + for (ExPolygon &shape : shapes) shape.translate(-bounding_box.min); + for (const ExPolygon &shape : shapes) r->draw(shape); + + // copy rastered data to pixels + sla::RasterEncoder encoder = + [&pix = m_result, w = m_tex_size.x(), h = m_tex_size.y(), + gray_level = m_input.gray_level] + (const void *ptr, size_t width, size_t height, size_t num_components) { + size_t size {static_cast(w*h)}; + const unsigned char *ptr2 = (const unsigned char *) ptr; + for (size_t x = 0; x < width; ++x) + for (size_t y = 0; y < height; ++y) { + size_t index = y*w + x; + assert(index < size); + if (index >= size) continue; + pix[3+4*index] = ptr2[y * width + x] / gray_level; + } + return sla::EncodedRaster(); + }; + r->encode(encoder); +} + +void CreateFontImageJob::finalize(bool canceled, std::exception_ptr &) +{ + if (m_input.count_opened_font_files) + --(*m_input.count_opened_font_files); + if (canceled || m_input.cancel->load()) return; + + *m_input.is_created = true; + + // Exist result bitmap with preview? + // (not valid input. e.g. not loadable font) + if (m_result.empty()) { + // TODO: write text cannot load into texture + m_result = std::vector(m_tex_size.x() * m_tex_size.y() * 4, {255}); + } + + // upload texture on GPU + const GLenum target = GL_TEXTURE_2D; + glsafe(::glBindTexture(target, m_input.texture_id)); + + GLint + w = m_tex_size.x(), h = m_tex_size.y(), + xoffset = m_input.size.x() - m_tex_size.x(), // arrange right + yoffset = m_input.size.y() * m_input.index; + glsafe(::glTexSubImage2D(target, m_input.level, xoffset, yoffset, w, h, + m_input.format, m_input.type, m_result.data())); + + // bind default texture + GLuint no_texture_id = 0; + glsafe(::glBindTexture(target, no_texture_id)); + + // show rendered texture + wxGetApp().plater()->canvas3D()->schedule_extra_frame(0); + + BOOST_LOG_TRIVIAL(info) + << "Generate Preview font('" << m_input.font_name << "' id:" << m_input.index << ") " + << "with text: '" << m_input.text << "' " + << "texture_size " << m_input.size.x() << " x " << m_input.size.y(); +} \ No newline at end of file diff --git a/src/slic3r/GUI/Jobs/CreateFontNameImageJob.hpp b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.hpp new file mode 100644 index 0000000000..da96b26969 --- /dev/null +++ b/src/slic3r/GUI/Jobs/CreateFontNameImageJob.hpp @@ -0,0 +1,75 @@ +#ifndef slic3r_CreateFontNameImageJob_hpp_ +#define slic3r_CreateFontNameImageJob_hpp_ + +#include +#include +#include +#include +#include +#include "Job.hpp" +#include "libslic3r/Point.hpp" // Vec2i + +namespace Slic3r::GUI { + +/// +/// Keep data for rasterization of text by font face +/// +struct FontImageData +{ + // Text to rasterize + std::string text; + // Define font face + wxString font_name; + wxFontEncoding encoding; + // texture for copy result to + // texture MUST BE initialized + GLuint texture_id; + // Index of face name, define place in texture + size_t index; + // Height of each text + // And Limit for width + Vec2i size; // in px + + // bigger value create darker image + // divide value 255 + unsigned char gray_level = 5; + + // texture meta data + GLenum format = GL_ALPHA, type = GL_UNSIGNED_BYTE; + GLint level = 0; + + // prevent opening too much files + // it is decreased in finalize phase + unsigned int *count_opened_font_files = nullptr; + + std::shared_ptr> cancel = nullptr; + std::shared_ptr is_created = nullptr; +}; + +/// +/// Create image for face name +/// +class CreateFontImageJob : public Job +{ + FontImageData m_input; + std::vector m_result; + Point m_tex_size; +public: + CreateFontImageJob(FontImageData &&input); + /// + /// Rasterize text into image (result) + /// + /// Check for cancelation + void process(Ctl &ctl) override; + + /// + /// Copy image data into OpenGL texture + /// + /// + /// + void finalize(bool canceled, std::exception_ptr &) override; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_CreateFontNameImageJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp new file mode 100644 index 0000000000..d1a671330a --- /dev/null +++ b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.cpp @@ -0,0 +1,164 @@ +#include "CreateFontStyleImagesJob.hpp" + +// rasterization of ExPoly +#include "libslic3r/SLA/AGGRaster.hpp" + +// for get DPI +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/MainFrame.hpp" + +#include "slic3r/GUI/3DScene.hpp" // ::glsafe + +// ability to request new frame after finish rendering +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; +using namespace Slic3r::GUI::Emboss; + + +CreateFontStyleImagesJob::CreateFontStyleImagesJob( + StyleManager::StyleImagesData &&input) + : m_input(std::move(input)) +{ + assert(m_input.result != nullptr); + assert(!m_input.styles.empty()); + assert(!m_input.text.empty()); + assert(m_input.max_size.x() > 1); + assert(m_input.max_size.y() > 1); +} + +void CreateFontStyleImagesJob::process(Ctl &ctl) +{ + // create shapes and calc size (bounding boxes) + std::vector name_shapes(m_input.styles.size()); + std::vector scales(m_input.styles.size()); + images = std::vector(m_input.styles.size()); + + for (auto &item : m_input.styles) { + size_t index = &item - &m_input.styles.front(); + ExPolygons &shapes = name_shapes[index]; + shapes = text2shapes(item.font, m_input.text.c_str(), item.prop); + + // create image description + StyleManager::StyleImage &image = images[index]; + BoundingBox &bounding_box = image.bounding_box; + for (ExPolygon &shape : shapes) + bounding_box.merge(BoundingBox(shape.contour.points)); + for (ExPolygon &shape : shapes) shape.translate(-bounding_box.min); + + // calculate conversion from FontPoint to screen pixels by size of font + auto mf = wxGetApp().mainframe; + // dot per inch for monitor + int dpi = get_dpi_for_window(mf); + double ppm = dpi / 25.4; // pixel per milimeter + const auto &cn = item.prop.collection_number; + unsigned int font_index = (cn.has_value()) ? *cn : 0; + double unit_per_em = item.font.font_file->infos[font_index].unit_per_em; + double scale = item.prop.size_in_mm / unit_per_em * SHAPE_SCALE * ppm; + scales[index] = scale; + + //double scale = font_prop.size_in_mm * SCALING_FACTOR; + BoundingBoxf bb2(bounding_box.min.cast(), + bounding_box.max.cast()); + bb2.scale(scale); + image.tex_size.x = std::ceil(bb2.max.x() - bb2.min.x()); + image.tex_size.y = std::ceil(bb2.max.y() - bb2.min.y()); + + // crop image width + if (image.tex_size.x > m_input.max_size.x()) + image.tex_size.x = m_input.max_size.x(); + // crop image height + if (image.tex_size.y > m_input.max_size.y()) + image.tex_size.y = m_input.max_size.y(); + } + + // arrange bounding boxes + int offset_y = 0; + width = 0; + for (StyleManager::StyleImage &image : images) { + image.offset.y() = offset_y; + offset_y += image.tex_size.y+1; + if (width < image.tex_size.x) + width = image.tex_size.x; + } + height = offset_y; + for (StyleManager::StyleImage &image : images) { + const Point &o = image.offset; + const ImVec2 &s = image.tex_size; + image.uv0 = ImVec2(o.x() / (double) width, + o.y() / (double) height); + image.uv1 = ImVec2((o.x() + s.x) / (double) width, + (o.y() + s.y) / (double) height); + } + + // Set up result + pixels = std::vector(4*width * height, {255}); + + // upload sub textures + for (StyleManager::StyleImage &image : images) { + sla::Resolution resolution(image.tex_size.x, image.tex_size.y); + size_t index = &image - &images.front(); + double pixel_dim = SCALING_FACTOR / scales[index]; + sla::PixelDim dim(pixel_dim, pixel_dim); + double gamma = 1.; + std::unique_ptr r = + sla::create_raster_grayscale_aa(resolution, dim, gamma); + for (const ExPolygon &shape : name_shapes[index]) r->draw(shape); + + // copy rastered data to pixels + sla::RasterEncoder encoder = [&offset = image.offset, &pix = pixels, w=width,h=height] + (const void *ptr, size_t width, size_t height, size_t num_components) { + // bigger value create darker image + unsigned char gray_level = 5; + size_t size {static_cast(w*h)}; + assert((offset.x() + width) <= (size_t)w); + assert((offset.y() + height) <= (size_t)h); + const unsigned char *ptr2 = (const unsigned char *) ptr; + for (size_t x = 0; x < width; ++x) + for (size_t y = 0; y < height; ++y) { + size_t index = (offset.y() + y)*w + offset.x() + x; + assert(index < size); + if (index >= size) continue; + pix[4*index+3] = ptr2[y * width + x] / gray_level; + } + return sla::EncodedRaster(); + }; + r->encode(encoder); + } +} + +void CreateFontStyleImagesJob::finalize(bool canceled, std::exception_ptr &) +{ + if (canceled) return; + // upload texture on GPU + GLuint tex_id; + GLenum target = GL_TEXTURE_2D, format = GL_RGBA, type = GL_UNSIGNED_BYTE; + GLint level = 0, border = 0; + glsafe(::glGenTextures(1, &tex_id)); + glsafe(::glBindTexture(target, tex_id)); + glsafe(::glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST)); + glsafe(::glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST)); + GLint w = width, h=height; + glsafe(::glTexImage2D(target, level, GL_RGBA, w, h, border, format, type, + (const void *) pixels.data())); + + // set up texture id + void *texture_id = (void *) (intptr_t) tex_id; + for (StyleManager::StyleImage &image : images) + image.texture_id = texture_id; + + // move to result + m_input.result->styles = std::move(m_input.styles); + m_input.result->images = std::move(images); + + // bind default texture + GLuint no_texture_id = 0; + glsafe(::glBindTexture(target, no_texture_id)); + + // show rendered texture + wxGetApp().plater()->canvas3D()->schedule_extra_frame(0); +} \ No newline at end of file diff --git a/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp new file mode 100644 index 0000000000..c220f2ee01 --- /dev/null +++ b/src/slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp @@ -0,0 +1,36 @@ +#ifndef slic3r_CreateFontStyleImagesJob_hpp_ +#define slic3r_CreateFontStyleImagesJob_hpp_ + +#include +#include +#include +#include "slic3r/Utils/EmbossStyleManager.hpp" +#include "Job.hpp" + +namespace Slic3r::GUI::Emboss { + +/// +/// Create texture with name of styles written by its style +/// NOTE: Access to glyph cache is possible only from job +/// +class CreateFontStyleImagesJob : public Job +{ + StyleManager::StyleImagesData m_input; + + // Output data + // texture size + int width, height; + // texture data + std::vector pixels; + // descriptors of sub textures + std::vector images; + +public: + CreateFontStyleImagesJob(StyleManager::StyleImagesData &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &) override; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_CreateFontStyleImagesJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp new file mode 100644 index 0000000000..56d8c7e454 --- /dev/null +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -0,0 +1,791 @@ +#include "EmbossJob.hpp" + +#include + +#include +#include // load_obj for default mesh +#include // use surface cuts + +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" +#include "slic3r/GUI/CameraUtils.hpp" +#include "slic3r/GUI/format.hpp" +#include "slic3r/Utils/UndoRedo.hpp" + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI; +using namespace Slic3r::GUI::Emboss; + +// private namespace +namespace priv{ +// create sure that emboss object is bigger than source object [in mm] +constexpr float safe_extension = 1.0f; + +/// +/// Assert check of inputs data +/// +/// +/// +bool check(const DataBase &input, bool check_fontfile = true, bool use_surface = false); +bool check(const DataCreateVolume &input, bool is_main_thread = false); +bool check(const DataCreateObject &input); +bool check(const DataUpdate &input, bool is_main_thread = false, bool use_surface = false); +bool check(const CreateSurfaceVolumeData &input, bool is_main_thread = false); +bool check(const UpdateSurfaceVolumeData &input, bool is_main_thread = false); + +// +/// Try to create mesh from text +/// +/// Text to convert on mesh +/// + Shape of characters + Property of font +/// Font file with cache +/// NOTE: Cache glyphs is changed +/// To check if process was canceled +/// Triangle mesh model +template static TriangleMesh try_create_mesh(const DataBase &input, FontFileWithCache &font, Fnc was_canceled); +template static TriangleMesh create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl &ctl); + +/// +/// Create default mesh for embossed text +/// +/// Not empty model(index trinagle set - its) +static TriangleMesh create_default_mesh(); + +/// +/// Must be called on main thread +/// +/// New mesh data +/// Text configuration, ... +static void update_volume(TriangleMesh &&mesh, const DataUpdate &data); + +/// +/// Add new volume to object +/// +/// triangles of new volume +/// Object where to add volume +/// Type of new volume +/// Transformation of volume inside of object +/// Text configuration and New VolumeName +static void create_volume(TriangleMesh &&mesh, const ObjectID& object_id, + const ModelVolumeType type, const Transform3d trmat, const DataBase &data); + +/// +/// Select Volume from objects +/// +/// All objects in scene +/// Identifier of volume in object +/// Pointer to volume when exist otherwise nullptr +static ModelVolume *get_volume(ModelObjectPtrs &objects, const ObjectID &volume_id); + +/// +/// Create projection for cut surface from mesh +/// +/// Volume transformation in object +/// Convert shape to milimeters +/// Bounding box 3d of model volume for projection ranges +/// Orthogonal cut_projection +static OrthoProject create_projection_for_cut(Transform3d tr, double shape_scale, const std::pair &z_range); + +/// +/// Create tranformation for emboss Cutted surface +/// +/// True .. raise, False .. engrave +/// Depth of embossing +/// Text voliume transformation inside object +/// Cutted surface from model +/// Projection +static OrthoProject3d create_emboss_projection(bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut); + +/// +/// Cut surface into triangle mesh +/// +/// (can't be const - cache of font) +/// SurfaceVolume data +/// Check to interupt execution +/// Extruded object from cuted surace +static TriangleMesh cut_surface(/*const*/ DataBase &input1, const SurfaceVolumeData &input2, std::function was_canceled); + +static void create_message(const std::string &message); // only in finalize +static bool process(std::exception_ptr &eptr); + +class JobException : public std::runtime_error { +public: JobException(const char* message):runtime_error(message){}}; + +}// namespace priv + +///////////////// +/// Create Volume +CreateVolumeJob::CreateVolumeJob(DataCreateVolume &&input) + : m_input(std::move(input)) +{ + assert(priv::check(m_input, true)); +} + +void CreateVolumeJob::process(Ctl &ctl) { + if (!priv::check(m_input)) throw std::runtime_error("Bad input data for EmbossCreateVolumeJob."); + auto was_canceled = [&ctl]()->bool { return ctl.was_canceled(); }; + m_result = priv::create_mesh(m_input, was_canceled, ctl); + // center result + Vec3f c = m_result.bounding_box().center().cast(); + if (!c.isApprox(Vec3f::Zero())) m_result.translate(-c); +} + +void CreateVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { + // doesn't care about exception when process was canceled by user + if (canceled) { + eptr = nullptr; + return; + } + if (priv::process(eptr)) return; + if (m_result.its.empty()) + return priv::create_message(_u8L("Can't create empty volume.")); + + priv::create_volume(std::move(m_result), m_input.object_id, m_input.volume_type, m_input.trmat, m_input); +} + + +///////////////// +/// Create Object +CreateObjectJob::CreateObjectJob(DataCreateObject &&input) + : m_input(std::move(input)) +{ + assert(priv::check(m_input)); +} + +void CreateObjectJob::process(Ctl &ctl) +{ + if (!priv::check(m_input)) + throw std::runtime_error("Bad input data for EmbossCreateObjectJob."); + + auto was_canceled = [&ctl]()->bool { return ctl.was_canceled(); }; + m_result = priv::create_mesh(m_input, was_canceled, ctl); + if (was_canceled()) return; + + // Create new object + // calculate X,Y offset position for lay on platter in place of + // mouse click + Vec2d bed_coor = CameraUtils::get_z0_position( + m_input.camera, m_input.screen_coor); + + // check point is on build plate: + Points bed_shape_; + bed_shape_.reserve(m_input.bed_shape.size()); + for (const Vec2d &p : m_input.bed_shape) + bed_shape_.emplace_back(p.cast()); + Polygon bed(bed_shape_); + if (!bed.contains(bed_coor.cast())) + // mouse pose is out of build plate so create object in center of plate + bed_coor = bed.centroid().cast(); + + double z = m_input.text_configuration.style.prop.emboss / 2; + Vec3d offset(bed_coor.x(), bed_coor.y(), z); + offset -= m_result.center(); + Transform3d::TranslationType tt(offset.x(), offset.y(), offset.z()); + m_transformation = Transform3d(tt); +} + +void CreateObjectJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + // doesn't care about exception when process was canceled by user + if (canceled) { + eptr = nullptr; + return; + } + if (priv::process(eptr)) return; + + // only for sure + if (m_result.empty()) + return priv::create_message(_u8L("Can't create empty object.")); + + GUI_App &app = wxGetApp(); + Plater *plater = app.plater(); + ObjectList *obj_list = app.obj_list(); + GLCanvas3D *canvas = plater->canvas3D(); + + plater->take_snapshot(_L("Add Emboss text object")); + + // Create new object and change selection + bool center = false; + obj_list->load_mesh_object(std::move(m_result), m_input.volume_name, + center, &m_input.text_configuration, + &m_transformation); + + // When add new object selection is empty. + // When cursor move and no one object is selected than + // Manager::reset_all() So Gizmo could be closed before end of creation object + GLGizmosManager &manager = canvas->get_gizmos_manager(); + if (manager.get_current_type() != GLGizmosManager::Emboss) + manager.open_gizmo(GLGizmosManager::Emboss); + + // redraw scene + canvas->reload_scene(true); +} + +///////////////// +/// Update Volume +UpdateJob::UpdateJob(DataUpdate&& input) + : m_input(std::move(input)) +{ + assert(priv::check(m_input, true)); +} + +void UpdateJob::process(Ctl &ctl) +{ + if (!priv::check(m_input)) + throw std::runtime_error("Bad input data for EmbossUpdateJob."); + + auto was_canceled = [&ctl, &cancel = m_input.cancel]()->bool { + if (cancel->load()) return true; + return ctl.was_canceled(); + }; + m_result = priv::try_create_mesh(m_input, m_input.font_file, was_canceled); + if (was_canceled()) return; + if (m_result.its.empty()) + throw priv::JobException(_u8L("Created text volume is empty. Change text or font.").c_str()); + + // center triangle mesh + Vec3d shift = m_result.bounding_box().center(); + m_result.translate(-shift.cast()); +} + +void UpdateJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + // doesn't care about exception when process was canceled by user + if (canceled || m_input.cancel->load()) { + eptr = nullptr; + return; + } + if (priv::process(eptr)) return; + priv::update_volume(std::move(m_result), m_input); +} + +namespace Slic3r::GUI::Emboss { + +SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id) +{ + SurfaceVolumeData::ModelSources result; + result.reserve(volumes.size() - 1); + for (const ModelVolume *v : volumes) { + if (text_volume_id.has_value() && v->id().id == *text_volume_id) continue; + // skip modifiers and negative volumes, ... + if (!v->is_model_part()) continue; + const TriangleMesh &tm = v->mesh(); + if (tm.empty()) continue; + if (tm.its.empty()) continue; + result.push_back({v->get_mesh_shared_ptr(), v->get_matrix()}); + } + return result; +} + +SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume *text_volume) +{ + if (text_volume == nullptr) return {}; + if (!text_volume->text_configuration.has_value()) return {}; + const ModelVolumePtrs &volumes = text_volume->get_object()->volumes; + // no other volume in object + if (volumes.size() <= 1) return {}; + return create_sources(volumes, text_volume->id().id); +} + +} // namespace Slic3r::GUI::Emboss + +///////////////// +/// Create Surface volume +CreateSurfaceVolumeJob::CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input) + : m_input(std::move(input)) +{ + assert(priv::check(m_input, true)); +} + +void CreateSurfaceVolumeJob::process(Ctl &ctl) { + if (!priv::check(m_input)) + throw std::runtime_error("Bad input data for CreateSurfaceVolumeJob."); + // check cancelation of process + auto was_canceled = [&ctl]() -> bool { return ctl.was_canceled(); }; + m_result = priv::cut_surface(m_input, m_input, was_canceled); +} + +void CreateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) { + // doesn't care about exception when process was canceled by user + if (canceled) return; + if (priv::process(eptr)) return; + + // TODO: Find better way to Not center volume data when add !!! + TriangleMesh mesh = m_result; // Part1: copy + + priv::create_volume(std::move(m_result), m_input.object_id, + m_input.volume_type, m_input.text_tr, m_input); + + // Part2: update volume data + //auto vol = wxGetApp().plater()->model().objects[m_input.object_idx]->volumes.back(); + //UpdateJob::update_volume(vol, std::move(mesh), m_input.text_configuration, m_input.volume_name); +} + +///////////////// +/// Cut Surface +UpdateSurfaceVolumeJob::UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input) + : m_input(std::move(input)) +{ + assert(priv::check(m_input, true)); +} + +void UpdateSurfaceVolumeJob::process(Ctl &ctl) +{ + if (!priv::check(m_input)) + throw std::runtime_error("Bad input data for UseSurfaceJob."); + + // check cancelation of process + auto was_canceled = [&ctl, &cancel = m_input.cancel]()->bool { + if (cancel->load()) return true; + return ctl.was_canceled(); + }; + m_result = priv::cut_surface(m_input, m_input, was_canceled); +} + +void UpdateSurfaceVolumeJob::finalize(bool canceled, std::exception_ptr &eptr) +{ + // doesn't care about exception when process was canceled by user + if (m_input.cancel->load()) { + eptr = nullptr; + return; + } + if (canceled) return; + if (priv::process(eptr)) return; + priv::update_volume(std::move(m_result), m_input); +} + +//////////////////////////// +/// private namespace implementation +bool priv::check(const DataBase &input, bool check_fontfile, bool use_surface) +{ + bool res = true; + if (check_fontfile) { + assert(input.font_file.has_value()); + res &= input.font_file.has_value(); + } + assert(!input.text_configuration.fix_3mf_tr.has_value()); + res &= !input.text_configuration.fix_3mf_tr.has_value(); + assert(!input.text_configuration.text.empty()); + res &= !input.text_configuration.text.empty(); + assert(!input.volume_name.empty()); + res &= !input.volume_name.empty(); + assert(input.text_configuration.style.prop.use_surface == use_surface); + res &= input.text_configuration.style.prop.use_surface == use_surface; + return res; +} +bool priv::check(const DataCreateVolume &input, bool is_main_thread) { + bool check_fontfile = false; + bool res = check((DataBase) input, check_fontfile); + assert(input.volume_type != ModelVolumeType::INVALID); + res &= input.volume_type != ModelVolumeType::INVALID; + assert(input.object_id.id >= 0); + res &= input.object_id.id >= 0; + return res; +} +bool priv::check(const DataCreateObject &input) { + bool check_fontfile = false; + bool res = check((DataBase) input, check_fontfile); + assert(input.screen_coor.x() >= 0.); + res &= input.screen_coor.x() >= 0.; + assert(input.screen_coor.y() >= 0.); + res &= input.screen_coor.y() >= 0.; + assert(input.bed_shape.size() >= 3); // at least triangle + res &= input.bed_shape.size() >= 3; + return res; +} +bool priv::check(const DataUpdate &input, bool is_main_thread, bool use_surface){ + bool check_fontfile = true; + bool res = check((DataBase) input, check_fontfile, use_surface); + assert(input.volume_id.id >= 0); + res &= input.volume_id.id >= 0; + if (is_main_thread) + assert(get_volume(wxGetApp().model().objects, input.volume_id) != nullptr); + assert(input.cancel != nullptr); + res &= input.cancel != nullptr; + if (is_main_thread) + assert(!input.cancel->load()); + return res; +} +bool priv::check(const CreateSurfaceVolumeData &input, bool is_main_thread) +{ + bool use_surface = true; + bool res = check((DataBase)input, is_main_thread, use_surface); + assert(!input.sources.empty()); + res &= !input.sources.empty(); + return res; +} +bool priv::check(const UpdateSurfaceVolumeData &input, bool is_main_thread){ + bool use_surface = true; + bool res = check((DataUpdate)input, is_main_thread, use_surface); + assert(!input.sources.empty()); + res &= !input.sources.empty(); + return res; +} + +template +TriangleMesh priv::try_create_mesh(const DataBase &input, FontFileWithCache &font, Fnc was_canceled) +{ + const TextConfiguration &tc = input.text_configuration; + const char *text = tc.text.c_str(); + const FontProp &prop = tc.style.prop; + + assert(font.has_value()); + if (!font.has_value()) return {}; + + ExPolygons shapes = text2shapes(font, text, prop, was_canceled); + if (shapes.empty()) return {}; + if (was_canceled()) return {}; + + const auto &cn = prop.collection_number; + unsigned int font_index = (cn.has_value()) ? *cn : 0; + assert(font_index < font.font_file->infos.size()); + int unit_per_em = font.font_file->infos[font_index].unit_per_em; + float scale = prop.size_in_mm / unit_per_em; + float depth = prop.emboss / scale; + auto projectZ = std::make_unique(depth); + ProjectScale project(std::move(projectZ), scale); + if (was_canceled()) return {}; + return TriangleMesh(polygons2model(shapes, project)); +} + +template +TriangleMesh priv::create_mesh(DataBase &input, Fnc was_canceled, Job::Ctl& ctl) +{ + // It is neccessary to create some shape + // Emboss text window is opened by creation new emboss text object + TriangleMesh result; + if (input.font_file.has_value()) { + result = try_create_mesh(input, input.font_file, was_canceled); + if (was_canceled()) return {}; + } + + if (result.its.empty()) { + result = priv::create_default_mesh(); + if (was_canceled()) return {}; + // only info + ctl.call_on_main_thread([]() { + create_message(_u8L("It is used default volume for embossed " + "text, try to change text or font for fix it.")); + }); + } + + assert(!result.its.empty()); + return result; +} + +TriangleMesh priv::create_default_mesh() +{ + // When cant load any font use default object loaded from file + std::string path = Slic3r::resources_dir() + "/data/embossed_text.stl"; + TriangleMesh triangle_mesh; + if (!load_obj(path.c_str(), &triangle_mesh)) { + // when can't load mesh use cube + return TriangleMesh(its_make_cube(36., 4., 2.5)); + } + return triangle_mesh; +} + +void UpdateJob::update_volume(ModelVolume *volume, + TriangleMesh &&mesh, + const TextConfiguration &text_configuration, + const std::string &volume_name) +{ + // check inputs + bool is_valid_input = + volume != nullptr && + !mesh.empty() && + !volume_name.empty(); + assert(is_valid_input); + if (!is_valid_input) return; + + // update volume + volume->set_mesh(std::move(mesh)); + volume->set_new_unique_id(); + volume->calculate_convex_hull(); + volume->get_object()->invalidate_bounding_box(); + volume->text_configuration = text_configuration; + + GUI_App &app = wxGetApp(); // may be move to input + GLCanvas3D *canvas = app.plater()->canvas3D(); + const Selection &selection = canvas->get_selection(); + const GLVolume *gl_volume = selection.get_volume(*selection.get_volume_idxs().begin()); + int object_idx = gl_volume->object_idx(); + + if (volume->name != volume_name) { + volume->name = volume_name; + + // update volume name in right panel( volume / object name) + int volume_idx = gl_volume->volume_idx(); + ObjectList *obj_list = app.obj_list(); + obj_list->update_name_in_list(object_idx, volume_idx); + } + + // update printable state on canvas + if (volume->type() == ModelVolumeType::MODEL_PART) + canvas->update_instance_printable_state_for_object((size_t) object_idx); + + // Move object on bed + if (GLGizmoEmboss::is_text_object(volume)) volume->get_object()->ensure_on_bed(); + + // redraw scene + bool refresh_immediately = false; + canvas->reload_scene(refresh_immediately); +} + +void priv::update_volume(TriangleMesh &&mesh, const DataUpdate &data) +{ + // for sure that some object will be created + if (mesh.its.empty()) + return priv::create_message("Empty mesh can't be created."); + + Plater *plater = wxGetApp().plater(); + GLCanvas3D *canvas = plater->canvas3D(); + + // Check emboss gizmo is still open + GLGizmosManager &manager = canvas->get_gizmos_manager(); + if (manager.get_current_type() != GLGizmosManager::Emboss) return; + + std::string snap_name = GUI::format(_L("Text: %1%"), data.text_configuration.text); + Plater::TakeSnapshot snapshot(plater, snap_name, UndoRedo::SnapshotType::GizmoAction); + ModelVolume *volume = get_volume(plater->model().objects, data.volume_id); + // could appear when user delete edited volume + if (volume == nullptr) + return; + + // apply fix matrix made by store to .3mf + const auto &tc = volume->text_configuration; + assert(tc.has_value()); + if (tc.has_value() && tc->fix_3mf_tr.has_value()) + volume->set_transformation(volume->get_matrix() * tc->fix_3mf_tr->inverse()); + + UpdateJob::update_volume(volume, std::move(mesh), data.text_configuration, data.volume_name); +} + +void priv::create_volume( + TriangleMesh &&mesh, const ObjectID& object_id, + const ModelVolumeType type, const Transform3d trmat, const DataBase &data) +{ + GUI_App &app = wxGetApp(); + Plater *plater = app.plater(); + ObjectList *obj_list = app.obj_list(); + GLCanvas3D *canvas = plater->canvas3D(); + ModelObjectPtrs &objects = plater->model().objects; + + ModelObject *obj = nullptr; + size_t object_idx = 0; + for (; object_idx < objects.size(); ++object_idx) { + ModelObject *o = objects[object_idx]; + if (o->id() == object_id) { + obj = o; + break; + } + } + + // Parent object for text volume was propably removed. + // Assumption: User know what he does, so text volume is no more needed. + if (obj == nullptr) + return priv::create_message(_u8L("Bad object to create volume.")); + + if (mesh.its.empty()) + return priv::create_message(_u8L("Can't create empty volume.")); + + plater->take_snapshot(_L("Add Emboss text Volume")); + + // NOTE: be carefull add volume also center mesh !!! + // So first add simple shape(convex hull is also calculated) + ModelVolume *volume = obj->add_volume(make_cube(1., 1., 1.), type); + + // TODO: Refactor to create better way to not set cube at begining + // Revert mesh centering by set mesh after add cube + volume->set_mesh(std::move(mesh)); + volume->calculate_convex_hull(); + + + // set a default extruder value, since user can't add it manually + volume->config.set_key_value("extruder", new ConfigOptionInt(0)); + + // do not allow model reload from disk + volume->source.is_from_builtin_objects = true; + + volume->name = data.volume_name; // copy + volume->text_configuration = data.text_configuration; // copy + volume->set_transformation(trmat); + + // update volume name in object list + // updata selection after new volume added + // change name of volume in right panel + // select only actual volume + // when new volume is created change selection to this volume + auto add_to_selection = [volume](const ModelVolume *vol) { return vol == volume; }; + wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(object_idx, add_to_selection); + if (!sel.IsEmpty()) obj_list->select_item(sel.front()); + + // update printable state on canvas + if (type == ModelVolumeType::MODEL_PART) canvas->update_instance_printable_state_for_object(object_idx); + + obj_list->selection_changed(); + + // Now is valid text volume selected open emboss gizmo + GLGizmosManager &manager = canvas->get_gizmos_manager(); + if (manager.get_current_type() != GLGizmosManager::Emboss) + manager.open_gizmo(GLGizmosManager::Emboss); + + // redraw scene + canvas->reload_scene(true); +} + +ModelVolume *priv::get_volume(ModelObjectPtrs &objects, + const ObjectID &volume_id) +{ + for (ModelObject *obj : objects) + for (ModelVolume *vol : obj->volumes) + if (vol->id() == volume_id) return vol; + return nullptr; +}; + +OrthoProject priv::create_projection_for_cut( + Transform3d tr, + double shape_scale, + const std::pair &z_range) +{ + double min_z = z_range.first - priv::safe_extension; + double max_z = z_range.second + priv::safe_extension; + assert(min_z < max_z); + // range between min and max value + double projection_size = max_z - min_z; + Matrix3d transformation_for_vector = tr.linear(); + // Projection must be negative value. + // System of text coordinate + // X .. from left to right + // Y .. from bottom to top + // Z .. from text to eye + Vec3d untransformed_direction(0., 0., projection_size); + Vec3d project_direction = transformation_for_vector * untransformed_direction; + + // Projection is in direction from far plane + tr.translate(Vec3d(0., 0., min_z)); + tr.scale(shape_scale); + return OrthoProject(tr, project_direction); +} + +OrthoProject3d priv::create_emboss_projection( + bool is_outside, float emboss, Transform3d tr, SurfaceCut &cut) +{ + // Offset of clossed side to model + const float surface_offset = 1e-3f; // [in mm] + float + front_move = (is_outside) ? emboss : surface_offset, + back_move = -((is_outside) ? surface_offset : emboss); + its_transform(cut, tr.pretranslate(Vec3d(0., 0., front_move))); + Vec3d from_front_to_back(0., 0., back_move - front_move); + return OrthoProject3d(from_front_to_back); +} + +// input can't be const - cache of font +TriangleMesh priv::cut_surface(DataBase& input1, const SurfaceVolumeData& input2, std::function was_canceled) +{ + const TextConfiguration &tc = input1.text_configuration; + const char *text = tc.text.c_str(); + const FontProp &fp = tc.style.prop; + + ExPolygons shapes = text2shapes(input1.font_file, text, fp, was_canceled); + if (shapes.empty() || shapes.front().contour.empty()) + throw JobException(_u8L("Font doesn't have any shape for given text.").c_str()); + + if (was_canceled()) return {}; + + // Define alignment of text - left, right, center, top bottom, .... + BoundingBox bb = get_extents(shapes); + Point projection_center = bb.center(); + for (ExPolygon &shape : shapes) shape.translate(-projection_center); + bb.translate(-projection_center); + + const FontFile &ff = *input1.font_file.font_file; + double shape_scale = get_shape_scale(fp, ff); + + const SurfaceVolumeData::ModelSources &sources = input2.sources; + const SurfaceVolumeData::ModelSource *biggest = nullptr; + + size_t biggest_count = 0; + // convert index from (s)ources to (i)ndexed (t)riangle (s)ets + std::vector s_to_itss(sources.size(), std::numeric_limits::max()); + std::vector itss; + itss.reserve(sources.size()); + for (const SurfaceVolumeData::ModelSource &s : sources) { + Transform3d mesh_tr_inv = s.tr.inverse(); + Transform3d cut_projection_tr = mesh_tr_inv * input2.text_tr; + std::pair z_range{0., 1.}; + OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range); + // copy only part of source model + indexed_triangle_set its = its_cut_AoI(s.mesh->its, bb, cut_projection); + if (its.indices.empty()) continue; + if (biggest_count < its.vertices.size()) { + biggest_count = its.vertices.size(); + biggest = &s; + } + s_to_itss[&s - &sources.front()] = itss.size(); + itss.emplace_back(std::move(its)); + } + if (itss.empty()) throw JobException(_u8L("There is no volume in projection direction.").c_str()); + + Transform3d tr_inv = biggest->tr.inverse(); + size_t itss_index = s_to_itss[biggest - &sources.front()]; + BoundingBoxf3 mesh_bb = bounding_box(itss[itss_index]); + for (const SurfaceVolumeData::ModelSource &s : sources) { + if (&s == biggest) continue; + size_t itss_index = s_to_itss[&s - &sources.front()]; + if (itss_index == std::numeric_limits::max()) continue; + Transform3d tr = s.tr * tr_inv; + indexed_triangle_set &its = itss[itss_index]; + its_transform(its, tr); + BoundingBoxf3 bb = bounding_box(its); + mesh_bb.merge(bb); + } + + // tr_inv = transformation of mesh inverted + Transform3d cut_projection_tr = tr_inv * input2.text_tr; + Transform3d emboss_tr = cut_projection_tr.inverse(); + BoundingBoxf3 mesh_bb_tr = mesh_bb.transformed(emboss_tr); + std::pair z_range{mesh_bb_tr.min.z(), mesh_bb_tr.max.z()}; + OrthoProject cut_projection = create_projection_for_cut(cut_projection_tr, shape_scale, z_range); + float projection_ratio = (-z_range.first + safe_extension) / (z_range.second - z_range.first + 2 * safe_extension); + + // Use CGAL to cut surface from triangle mesh + SurfaceCut cut = cut_surface(shapes, itss, cut_projection, projection_ratio); + if (cut.empty()) throw JobException(_u8L("There is no valid surface for text projection.").c_str()); + if (was_canceled()) return {}; + + // !! Projection needs to transform cut + OrthoProject3d projection = create_emboss_projection(input2.is_outside, fp.emboss, emboss_tr, cut); + + indexed_triangle_set new_its = cut2model(cut, projection); + assert(!new_its.empty()); + + if (was_canceled()) return {}; + return TriangleMesh(std::move(new_its)); +} + +bool priv::process(std::exception_ptr &eptr) { + if (!eptr) return false; + try { + std::rethrow_exception(eptr); + } catch (priv::JobException &e) { + create_message(e.what()); + eptr = nullptr; + } + return true; +} + +#include + +void priv::create_message(const std::string &message) { + wxMessageBox(wxString(message), _L("Issue during embossing the text."), + wxOK | wxICON_WARNING); +} diff --git a/src/slic3r/GUI/Jobs/EmbossJob.hpp b/src/slic3r/GUI/Jobs/EmbossJob.hpp new file mode 100644 index 0000000000..c8ef7fee64 --- /dev/null +++ b/src/slic3r/GUI/Jobs/EmbossJob.hpp @@ -0,0 +1,237 @@ +#ifndef slic3r_EmbossJob_hpp_ +#define slic3r_EmbossJob_hpp_ + +#include +#include +#include +#include +//#include +#include "slic3r/Utils/RaycastManager.hpp" +#include "slic3r/GUI/Camera.hpp" +#include "Job.hpp" + +namespace Slic3r { +class ModelVolume; +class TriangleMesh; +} + +namespace Slic3r::GUI::Emboss { + +/// +/// Base data holder for embossing +/// +struct DataBase +{ + // Keep pointer on Data of font (glyph shapes) + Slic3r::Emboss::FontFileWithCache font_file; + // font item is not used for create object + TextConfiguration text_configuration; + // new volume name created from text + std::string volume_name; +}; + +/// +/// Hold neccessary data to create ModelVolume in job +/// Volume is created on the surface of existing volume in object. +/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! +/// +struct DataCreateVolume : public DataBase +{ + // define embossed volume type + ModelVolumeType volume_type; + + // parent ModelObject index where to create volume + ObjectID object_id; + + // new created volume transformation + Transform3d trmat; +}; + +/// +/// Create new TextVolume on the surface of ModelObject +/// Should not be stopped +/// NOTE: EmbossDataBase::font_file doesn't have to be valid !!! +/// +class CreateVolumeJob : public Job +{ + DataCreateVolume m_input; + TriangleMesh m_result; + +public: + CreateVolumeJob(DataCreateVolume&& input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +/// +/// Hold neccessary data to create ModelObject in job +/// Object is placed on bed under screen coor +/// OR to center of scene when it is out of bed shape +/// +struct DataCreateObject : public DataBase +{ + // define position on screen where to create object + Vec2d screen_coor; + + // projection property + Camera camera; + + // shape of bed in case of create volume on bed + std::vector bed_shape; +}; + +/// +/// Create new TextObject on the platter +/// Should not be stopped +/// +class CreateObjectJob : public Job +{ + DataCreateObject m_input; + TriangleMesh m_result; + Transform3d m_transformation; +public: + CreateObjectJob(DataCreateObject&& input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +/// +/// Hold neccessary data to update embossed text object in job +/// +struct DataUpdate : public DataBase +{ + // unique identifier of volume to change + ObjectID volume_id; + + // flag that job is canceled + // for time after process. + std::shared_ptr> cancel; +}; + +/// +/// Update text shape in existing text volume +/// Predict that there is only one runnig(not canceled) instance of it +/// +class UpdateJob : public Job +{ + DataUpdate m_input; + TriangleMesh m_result; + +public: + // move params to private variable + UpdateJob(DataUpdate &&input); + + /// + /// Create new embossed volume by m_input data and store to m_result + /// + /// Control containing cancel flag + void process(Ctl &ctl) override; + + /// + /// Update volume - change object_id + /// + /// Was process canceled. + /// NOTE: Be carefull it doesn't care about + /// time between finished process and started finalize part. + /// unused + void finalize(bool canceled, std::exception_ptr &eptr) override; + + /// + /// Update text volume + /// + /// Volume to be updated + /// New Triangle mesh for volume + /// Parametric description of volume + /// Name of volume + static void update_volume(ModelVolume *volume, + TriangleMesh &&mesh, + const TextConfiguration &text_configuration, + const std::string &volume_name); +}; + +struct SurfaceVolumeData +{ + // Transformation of text volume inside of object + Transform3d text_tr; + + // Define projection move + // True (raised) .. move outside from surface + // False (engraved).. move into object + bool is_outside; + + struct ModelSource + { + // source volumes + std::shared_ptr mesh; + // Transformation of volume inside of object + Transform3d tr; + }; + using ModelSources = std::vector; + ModelSources sources; +}; + +/// +/// Hold neccessary data to create(cut) volume from surface object in job +/// +struct CreateSurfaceVolumeData : public DataBase, public SurfaceVolumeData{ + // define embossed volume type + ModelVolumeType volume_type; + + // parent ModelObject index where to create volume + ObjectID object_id; +}; + +/// +/// Cut surface from object and create cutted volume +/// Should not be stopped +/// +class CreateSurfaceVolumeJob : public Job +{ + CreateSurfaceVolumeData m_input; + TriangleMesh m_result; + +public: + CreateSurfaceVolumeJob(CreateSurfaceVolumeData &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +/// +/// Hold neccessary data to update embossed text object in job +/// +struct UpdateSurfaceVolumeData : public DataUpdate, public SurfaceVolumeData{}; + +/// +/// Copied triangles from object to be able create mesh for cut surface from +/// +/// Source object volumes for cut surface from +/// Source volume id +/// Source data for cut surface from +SurfaceVolumeData::ModelSources create_sources(const ModelVolumePtrs &volumes, std::optional text_volume_id = {}); + +/// +/// Copied triangles from object to be able create mesh for cut surface from +/// +/// Define text in object +/// Source data for cut surface from +SurfaceVolumeData::ModelSources create_volume_sources(const ModelVolume *text_volume); + + +/// +/// Update text volume to use surface from object +/// +class UpdateSurfaceVolumeJob : public Job +{ + UpdateSurfaceVolumeData m_input; + TriangleMesh m_result; + +public: + // move params to private variable + UpdateSurfaceVolumeJob(UpdateSurfaceVolumeData &&input); + void process(Ctl &ctl) override; + void finalize(bool canceled, std::exception_ptr &eptr) override; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_EmbossJob_hpp_ diff --git a/src/slic3r/GUI/Jobs/PlaterWorker.hpp b/src/slic3r/GUI/Jobs/PlaterWorker.hpp index 37b18b3b8f..a3559869a0 100644 --- a/src/slic3r/GUI/Jobs/PlaterWorker.hpp +++ b/src/slic3r/GUI/Jobs/PlaterWorker.hpp @@ -2,6 +2,7 @@ #define PLATERWORKER_HPP #include +#include #include "Worker.hpp" #include "BusyCursorJob.hpp" @@ -24,6 +25,7 @@ class PlaterWorker: public Worker { class PlaterJob : public Job { std::unique_ptr m_job; Plater *m_plater; + long long m_process_duration; // [ms] public: void process(Ctl &c) override @@ -55,12 +57,27 @@ class PlaterWorker: public Worker { } wctl{c}; CursorSetterRAII busycursor{wctl}; + + using namespace std::chrono; + steady_clock::time_point process_start = steady_clock::now(); m_job->process(wctl); + steady_clock::time_point process_end = steady_clock::now(); + m_process_duration = duration_cast(process_end - process_start).count(); } void finalize(bool canceled, std::exception_ptr &eptr) override { + using namespace std::chrono; + steady_clock::time_point finalize_start = steady_clock::now(); m_job->finalize(canceled, eptr); + steady_clock::time_point finalize_end = steady_clock::now(); + long long finalize_duration = duration_cast(finalize_end - finalize_start).count(); + + BOOST_LOG_TRIVIAL(info) + << std::fixed // do not use scientific notations + << "Job '" << typeid(*m_job).name() << "' " + << "spend " << m_process_duration + finalize_duration << "ms " + << "(process " << m_process_duration << "ms + finalize " << finalize_duration << "ms)"; if (eptr) try { std::rethrow_exception(eptr); diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 594e0716c6..b2e9bf0085 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -202,8 +202,14 @@ void NotificationManager::PopNotification::render(GLCanvas3D& canvas, float init if (m_id == 0) m_id = m_id_provider.allocate_id(); std::string name = "!!Ntfctn" + std::to_string(m_id); + + int flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoFocusOnAppearing; - if (imgui.begin(name, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { + if (imgui.begin(name, flags)) { ImVec2 win_size = ImGui::GetWindowSize(); render_left_sign(imgui); diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index cbad18d8a9..23d4d20b05 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -111,6 +111,8 @@ enum class NotificationType // Give user advice to simplify object with big amount of triangles // Contains ObjectID for closing when object is deleted SimplifySuggestion, + // Change of text will change font to similar one on. + UnknownFont, // information about netfabb is finished repairing model (blocking proccess) NetfabbFinished, // Short meesage to fill space between start and finish of export diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 066fc45223..cfc78f9f80 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -74,12 +74,14 @@ const std::map INFO_ITEMS{ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, + const bool is_text_volume, const wxString& extruder, const int idx/* = -1*/) : m_parent(parent), m_name(sub_obj_name), m_type(itVolume), m_volume_type(type), + m_is_text_volume(is_text_volume), m_idx(idx), m_extruder(type == Slic3r::ModelVolumeType::MODEL_PART || type == Slic3r::ModelVolumeType::PARAMETER_MODIFIER ? extruder : "") { @@ -330,6 +332,7 @@ static int get_root_idx(ObjectDataViewModelNode *parent_node, const ItemType roo ObjectDataViewModel::ObjectDataViewModel() { m_volume_bmps = MenuFactory::get_volume_bitmaps(); + m_text_volume_bmps = MenuFactory::get_text_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); m_lock_bmp = *get_bmp_bundle(LockIcon); @@ -352,7 +355,7 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node) bool is_volume_node = vol_type >= 0; if (!node->has_warning_icon() && !node->has_lock()) { - node->SetBitmap(is_volume_node ? *m_volume_bmps.at(vol_type) : m_empty_bmp); + node->SetBitmap(is_volume_node ? (node->is_text_volume() ? *m_text_volume_bmps.at(vol_type) : *m_volume_bmps.at(vol_type)) : m_empty_bmp); return; } @@ -373,7 +376,7 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode* node) if (node->has_lock()) bmps.emplace_back(&m_lock_bmp); if (is_volume_node) - bmps.emplace_back(m_volume_bmps[vol_type]); + bmps.emplace_back(node->is_text_volume() ? m_text_volume_bmps[vol_type] : m_volume_bmps[vol_type]); bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); } @@ -409,6 +412,7 @@ wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent const wxString &name, const int volume_idx, const Slic3r::ModelVolumeType volume_type, + const bool is_text_volume, const std::string& warning_icon_name, const wxString& extruder) { @@ -420,7 +424,7 @@ wxDataViewItem ObjectDataViewModel::AddVolumeChild( const wxDataViewItem &parent if (insert_position < 0) insert_position = get_root_idx(root, itInstanceRoot); - const auto node = new ObjectDataViewModelNode(root, name, volume_type, extruder, volume_idx); + const auto node = new ObjectDataViewModelNode(root, name, volume_type, is_text_volume, extruder, volume_idx); UpdateBitmapForNode(node, warning_icon_name, root->has_lock() && volume_type < ModelVolumeType::PARAMETER_MODIFIER); insert_position < 0 ? root->Append(node) : root->Insert(node, insert_position); @@ -769,8 +773,12 @@ wxDataViewItem ObjectDataViewModel::Delete(const wxDataViewItem &item) // get index of the last VolumeItem in CildrenList size_t vol_idx = GetItemIndexForFirstVolume(node_parent); - // delete this last volume ObjectDataViewModelNode *last_child_node = node_parent->GetNthChild(vol_idx); + // if last volume is text then don't delete it + if (last_child_node->is_text_volume()) + return parent; + + // delete this last volume DeleteSettings(wxDataViewItem(last_child_node)); node_parent->GetChildren().Remove(last_child_node); node_parent->m_volumes_cnt = 0; @@ -1616,22 +1624,6 @@ void ObjectDataViewModel::UpdateSettingsDigest(const wxDataViewItem &item, ItemChanged(item); } -void ObjectDataViewModel::SetVolumeType(const wxDataViewItem &item, const Slic3r::ModelVolumeType volume_type) -{ - if (!item.IsOk() || GetItemType(item) != itVolume) - return; - - ObjectDataViewModelNode *node = static_cast(item.GetID()); - node->SetVolumeType(volume_type); - node->SetBitmap(*m_volume_bmps[int(volume_type)]); - if (volume_type != Slic3r::ModelVolumeType::MODEL_PART && volume_type != Slic3r::ModelVolumeType::PARAMETER_MODIFIER) - node->SetExtruder(""); // hide extruder - else if (node->GetExtruder().IsEmpty()) - node->SetExtruder("default"); // show extruder ans set it to default - node->UpdateExtruderAndColorIcon(); - ItemChanged(item); -} - ModelVolumeType ObjectDataViewModel::GetVolumeType(const wxDataViewItem& item) { if (!item.IsOk() || GetItemType(item) != itVolume) @@ -1684,6 +1676,7 @@ wxDataViewItem ObjectDataViewModel::SetObjectPrintableState( void ObjectDataViewModel::UpdateBitmaps() { m_volume_bmps = MenuFactory::get_volume_bitmaps(); + m_text_volume_bmps = MenuFactory::get_text_volume_bitmaps(); m_warning_bmp = *get_bmp_bundle(WarningIcon); m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); m_lock_bmp = *get_bmp_bundle(LockIcon); diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 6f2d1519c5..993b67842a 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -84,6 +84,7 @@ class ObjectDataViewModelNode std::string m_action_icon_name = ""; ModelVolumeType m_volume_type{ -1 }; + bool m_is_text_volume{ false }; InfoItemType m_info_item_type {InfoItemType::Undef}; public: @@ -101,6 +102,7 @@ public: ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, + const bool is_text_volume, const wxString& extruder, const int idx = -1 ); @@ -191,7 +193,7 @@ public: InfoItemType GetInfoItemType() const { return m_info_item_type; } void SetIdx(const int& idx); int GetIdx() const { return m_idx; } - ModelVolumeType GetVolumeType() { return m_volume_type; } + ModelVolumeType GetVolumeType() const { return m_volume_type; } t_layer_height_range GetLayerRange() const { return m_layer_range; } wxString GetExtruder() { return m_extruder; } PrintIndicator IsPrintable() const { return m_printable; } @@ -235,6 +237,7 @@ public: void update_settings_digest_bitmaps(); bool update_settings_digest(const std::vector& categories); int volume_type() const { return int(m_volume_type); } + bool is_text_volume() const { return m_is_text_volume; } void sys_color_changed(); #ifndef NDEBUG @@ -261,6 +264,7 @@ class ObjectDataViewModel :public wxDataViewModel { std::vector m_objects; std::vector m_volume_bmps; + std::vector m_text_volume_bmps; std::map m_info_bmps; wxBitmapBundle m_empty_bmp; wxBitmapBundle m_warning_bmp; @@ -281,6 +285,7 @@ public: const wxString &name, const int volume_idx, const Slic3r::ModelVolumeType volume_type, + const bool is_text_volume, const std::string& warning_icon_name, const wxString& extruder); wxDataViewItem AddSettingsChild(const wxDataViewItem &parent_item); @@ -379,7 +384,6 @@ public: void UpdateObjectPrintable(wxDataViewItem parent_item); void UpdateInstancesPrintable(wxDataViewItem parent_item); - void SetVolumeType(const wxDataViewItem &item, const Slic3r::ModelVolumeType type); ModelVolumeType GetVolumeType(const wxDataViewItem &item); wxDataViewItem SetPrintableState( PrintIndicator printable, int obj_idx, int subobj_idx = -1, @@ -387,11 +391,9 @@ public: wxDataViewItem SetObjectPrintableState(PrintIndicator printable, wxDataViewItem obj_item); void SetAssociatedControl(wxDataViewCtrl* ctrl) { m_ctrl = ctrl; } - // Rescale bitmaps for existing Items + // Rescale bitmaps for existing Items void UpdateBitmaps(); - wxBitmapBundle GetVolumeIcon(const Slic3r::ModelVolumeType vol_type, - const std::string& warning_icon_name = std::string()); void AddWarningIcon(const wxDataViewItem& item, const std::string& warning_name); void DeleteWarningIcon(const wxDataViewItem& item, const bool unmark_object = false); void UpdateWarningIcon(const wxDataViewItem& item, const std::string& warning_name); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3204def994..e11e08d043 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4505,19 +4505,29 @@ void Plater::priv::on_right_click(RBtnEvent& evt) #else const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); #endif // ENABLE_WORLD_COORDINATE - menu = is_some_full_instances ? menus.object_menu() : - is_part ? menus.part_menu() : menus.multi_selection_menu(); + if (is_some_full_instances) + menu = menus.object_menu(); + else if (is_part) + menu = selection.is_single_text() ? menus.text_part_menu() : menus.part_menu(); + else + menu = menus.multi_selection_menu(); } } if (q != nullptr && menu) { + Vec2d mouse_position = evt.data.first; + wxPoint position(static_cast(mouse_position.x()), + static_cast(mouse_position.y())); #ifdef __linux__ - // For some reason on Linux the menu isn't displayed if position is specified - // (even though the position is sane). - q->PopupMenu(menu); -#else - q->PopupMenu(menu, (int)evt.data.first.x(), (int)evt.data.first.y()); + // For some reason on Linux the menu isn't displayed if position is + // specified (even though the position is sane). + position = wxDefaultPosition; #endif + GLCanvas3D &canvas = *q->canvas3D(); + canvas.apply_retina_scale(mouse_position); + canvas.set_popup_menu_position(mouse_position); + q->PopupMenu(menu, position); + canvas.clear_popup_menu_position(); } } @@ -4934,6 +4944,10 @@ bool Plater::priv::can_increase_instances() const || q->canvas3D()->get_gizmos_manager().is_in_editing_mode()) return false; + // Disallow arrange and add instance when emboss gizmo is opend + // Prevent strobo effect during editing emboss parameters. + if (q->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Emboss) return false; + const int obj_idx = get_selected_object_idx(); return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) && !sidebar->obj_list()->has_selected_cut_object(); @@ -4963,7 +4977,9 @@ bool Plater::priv::can_split_to_volumes() const bool Plater::priv::can_arrange() const { - return !model.objects.empty() && m_worker.is_idle(); + if (model.objects.empty() && m_worker.is_idle()) return false; + if (q->canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::Emboss) return false; + return true; } bool Plater::priv::can_layers_editing() const @@ -7244,7 +7260,7 @@ bool Plater::PopupMenu(wxMenu *menu, const wxPoint& pos) SuppressBackgroundProcessingUpdate sbpu; // When tracking a pop-up menu, postpone error messages from the slicing result. m_tracking_popup_menu = true; - bool out = this->wxPanel::PopupMenu(menu, pos); + bool out = this->wxPanel::PopupMenu(menu, pos); m_tracking_popup_menu = false; if (! m_tracking_popup_menu_error_message.empty()) { // Don't know whether the CallAfter is necessary, but it should not hurt. @@ -7262,6 +7278,7 @@ void Plater::bring_instance_forward() wxMenu* Plater::object_menu() { return p->menus.object_menu(); } wxMenu* Plater::part_menu() { return p->menus.part_menu(); } +wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); } wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); } wxMenu* Plater::default_menu() { return p->menus.default_menu(); } wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index ff750d561e..77a401f3f1 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -463,6 +463,7 @@ public: // get same Plater/ObjectList menus wxMenu* object_menu(); wxMenu* part_menu(); + wxMenu* text_part_menu(); wxMenu* sla_object_menu(); wxMenu* default_menu(); wxMenu* instance_menu(); diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 5e71d0817f..b27f7a7381 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1,3327 +1,3345 @@ -#include "libslic3r/libslic3r.h" -#include "Selection.hpp" - -#include "3DScene.hpp" -#include "GLCanvas3D.hpp" -#include "GUI_App.hpp" -#include "GUI.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "GUI_ObjectList.hpp" -#include "Camera.hpp" -#include "Plater.hpp" -#if ENABLE_WORLD_COORDINATE -#include "MsgDialog.hpp" -#endif // ENABLE_WORLD_COORDINATE - -#include "Gizmos/GLGizmoBase.hpp" - -#include "slic3r/Utils/UndoRedo.hpp" - -#include "libslic3r/LocalesUtils.hpp" -#include "libslic3r/Model.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/BuildVolume.hpp" - -#include - -#include -#include - -static const Slic3r::ColorRGBA UNIFORM_SCALE_COLOR = Slic3r::ColorRGBA::ORANGE(); -static const Slic3r::ColorRGBA SOLID_PLANE_COLOR = Slic3r::ColorRGBA::ORANGE(); -static const Slic3r::ColorRGBA TRANSPARENT_PLANE_COLOR = { 0.8f, 0.8f, 0.8f, 0.5f }; - -namespace Slic3r { -namespace GUI { - -Selection::VolumeCache::TransformCache::TransformCache(const Geometry::Transformation& transform) - : position(transform.get_offset()) - , rotation(transform.get_rotation()) - , scaling_factor(transform.get_scaling_factor()) - , mirror(transform.get_mirror()) - , full_matrix(transform.get_matrix()) -#if ENABLE_WORLD_COORDINATE - , transform(transform) - , rotation_matrix(transform.get_rotation_matrix()) - , scale_matrix(transform.get_scaling_factor_matrix()) - , mirror_matrix(transform.get_mirror_matrix()) -#endif // ENABLE_WORLD_COORDINATE -{ -#if !ENABLE_WORLD_COORDINATE - rotation_matrix = Geometry::assemble_transform(Vec3d::Zero(), rotation); - scale_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scaling_factor); - mirror_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), Vec3d::Ones(), mirror); -#endif // !ENABLE_WORLD_COORDINATE -} - -Selection::VolumeCache::VolumeCache(const Geometry::Transformation& volume_transform, const Geometry::Transformation& instance_transform) - : m_volume(volume_transform) - , m_instance(instance_transform) -{ -} - -bool Selection::Clipboard::is_sla_compliant() const -{ - if (m_mode == Selection::Volume) - return false; - - for (const ModelObject* o : m_model->objects) { - if (o->is_multiparts()) - return false; - - for (const ModelVolume* v : o->volumes) { - if (v->is_modifier()) - return false; - } - } - - return true; -} - -Selection::Clipboard::Clipboard() -{ - m_model.reset(new Model); -} - -void Selection::Clipboard::reset() -{ - m_model->clear_objects(); -} - -bool Selection::Clipboard::is_empty() const -{ - return m_model->objects.empty(); -} - -ModelObject* Selection::Clipboard::add_object() -{ - return m_model->add_object(); -} - -ModelObject* Selection::Clipboard::get_object(unsigned int id) -{ - return (id < (unsigned int)m_model->objects.size()) ? m_model->objects[id] : nullptr; -} - -const ModelObjectPtrs& Selection::Clipboard::get_objects() const -{ - return m_model->objects; -} - -Selection::Selection() - : m_volumes(nullptr) - , m_model(nullptr) - , m_enabled(false) - , m_mode(Instance) - , m_type(Empty) - , m_valid(false) - , m_scale_factor(1.0f) -{ - this->set_bounding_boxes_dirty(); -#if ENABLE_WORLD_COORDINATE - m_axes.set_stem_radius(0.15f); - m_axes.set_stem_length(3.0f); - m_axes.set_tip_radius(0.45f); - m_axes.set_tip_length(1.5f); -#endif // ENABLE_WORLD_COORDINATE -} - - -void Selection::set_volumes(GLVolumePtrs* volumes) -{ - m_volumes = volumes; - update_valid(); -} - -// Init shall be called from the OpenGL render function, so that the OpenGL context is initialized! -bool Selection::init() -{ - m_arrow.init_from(straight_arrow(10.0f, 5.0f, 5.0f, 10.0f, 1.0f)); - m_curved_arrow.init_from(circular_arrow(16, 10.0f, 5.0f, 10.0f, 5.0f, 1.0f)); -#if ENABLE_RENDER_SELECTION_CENTER - m_vbo_sphere.init_from(its_make_sphere(0.75, PI / 12.0)); -#endif // ENABLE_RENDER_SELECTION_CENTER - - return true; -} - -void Selection::set_model(Model* model) -{ - m_model = model; - update_valid(); -} - -void Selection::add(unsigned int volume_idx, bool as_single_selection, bool check_for_already_contained) -{ - if (!m_valid || (unsigned int)m_volumes->size() <= volume_idx) - return; - - const GLVolume* volume = (*m_volumes)[volume_idx]; - // wipe tower is already selected - if (is_wipe_tower() && volume->is_wipe_tower) - return; - - bool keep_instance_mode = (m_mode == Instance) && !as_single_selection; - bool already_contained = check_for_already_contained && contains_volume(volume_idx); - - // resets the current list if needed - bool needs_reset = as_single_selection && !already_contained; - needs_reset |= volume->is_wipe_tower; - needs_reset |= is_wipe_tower() && !volume->is_wipe_tower; - needs_reset |= as_single_selection && !is_any_modifier() && volume->is_modifier; - needs_reset |= is_any_modifier() && !volume->is_modifier; - - if (!already_contained || needs_reset) { - wxGetApp().plater()->take_snapshot(_L("Selection-Add"), UndoRedo::SnapshotType::Selection); - - if (needs_reset) - clear(); - - if (!keep_instance_mode) - m_mode = volume->is_modifier ? Volume : Instance; - } - else - // keep current mode - return; - - switch (m_mode) - { - case Volume: - { - if (volume->volume_idx() >= 0 && (is_empty() || volume->instance_idx() == get_instance_idx())) - do_add_volume(volume_idx); - - break; - } - case Instance: - { - Plater::SuppressSnapshots suppress(wxGetApp().plater()); - add_instance(volume->object_idx(), volume->instance_idx(), as_single_selection); - break; - } - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove(unsigned int volume_idx) -{ - if (!m_valid || (unsigned int)m_volumes->size() <= volume_idx) - return; - - if (!contains_volume(volume_idx)) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Remove"), UndoRedo::SnapshotType::Selection); - - GLVolume* volume = (*m_volumes)[volume_idx]; - - switch (m_mode) - { - case Volume: - { - do_remove_volume(volume_idx); - break; - } - case Instance: - { - do_remove_instance(volume->object_idx(), volume->instance_idx()); - break; - } - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_object(unsigned int object_idx, bool as_single_selection) -{ - if (!m_valid) - return; - - std::vector volume_idxs = get_volume_idxs_from_object(object_idx); - if ((!as_single_selection && contains_all_volumes(volume_idxs)) || - (as_single_selection && matches(volume_idxs))) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Add Object"), UndoRedo::SnapshotType::Selection); - - // resets the current list if needed - if (as_single_selection) - clear(); - - m_mode = Instance; - - do_add_volumes(volume_idxs); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_object(unsigned int object_idx) -{ - if (!m_valid) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Remove Object"), UndoRedo::SnapshotType::Selection); - - do_remove_object(object_idx); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_instance(unsigned int object_idx, unsigned int instance_idx, bool as_single_selection) -{ - if (!m_valid) - return; - - const std::vector volume_idxs = get_volume_idxs_from_instance(object_idx, instance_idx); - if ((!as_single_selection && contains_all_volumes(volume_idxs)) || - (as_single_selection && matches(volume_idxs))) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Add Instance"), UndoRedo::SnapshotType::Selection); - - // resets the current list if needed - if (as_single_selection) - clear(); - - m_mode = Instance; - - do_add_volumes(volume_idxs); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_instance(unsigned int object_idx, unsigned int instance_idx) -{ - if (!m_valid) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Remove Instance"), UndoRedo::SnapshotType::Selection); - - do_remove_instance(object_idx, instance_idx); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_volume(unsigned int object_idx, unsigned int volume_idx, int instance_idx, bool as_single_selection) -{ - if (!m_valid) - return; - - std::vector volume_idxs = get_volume_idxs_from_volume(object_idx, instance_idx, volume_idx); - if ((!as_single_selection && contains_all_volumes(volume_idxs)) || - (as_single_selection && matches(volume_idxs))) - return; - - // resets the current list if needed - if (as_single_selection) - clear(); - - m_mode = Volume; - - do_add_volumes(volume_idxs); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_volume(unsigned int object_idx, unsigned int volume_idx) -{ - if (!m_valid) - return; - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - GLVolume* v = (*m_volumes)[i]; - if (v->object_idx() == (int)object_idx && v->volume_idx() == (int)volume_idx) - do_remove_volume(i); - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_volumes(EMode mode, const std::vector& volume_idxs, bool as_single_selection) -{ - if (!m_valid) - return; - - if ((!as_single_selection && contains_all_volumes(volume_idxs)) || - (as_single_selection && matches(volume_idxs))) - return; - - // resets the current list if needed - if (as_single_selection) - clear(); - - m_mode = mode; - for (unsigned int i : volume_idxs) { - if (i < (unsigned int)m_volumes->size()) - do_add_volume(i); - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_volumes(EMode mode, const std::vector& volume_idxs) -{ - if (!m_valid) - return; - - m_mode = mode; - for (unsigned int i : volume_idxs) { - if (i < (unsigned int)m_volumes->size()) - do_remove_volume(i); - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_all() -{ - if (!m_valid) - return; - - unsigned int count = 0; - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - if (!(*m_volumes)[i]->is_wipe_tower) - ++count; - } - - if ((unsigned int)m_list.size() == count) - return; - - wxGetApp().plater()->take_snapshot(_(L("Selection-Add All")), UndoRedo::SnapshotType::Selection); - - m_mode = Instance; - clear(); - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - if (!(*m_volumes)[i]->is_wipe_tower) - do_add_volume(i); - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_all() -{ - if (!m_valid) - return; - - if (is_empty()) - return; - -// Not taking the snapshot with non-empty Redo stack will likely be more confusing than losing the Redo stack. -// Let's wait for user feedback. -// if (!wxGetApp().plater()->can_redo()) - wxGetApp().plater()->take_snapshot(_L("Selection-Remove All"), UndoRedo::SnapshotType::Selection); - - m_mode = Instance; - clear(); -} - -void Selection::set_deserialized(EMode mode, const std::vector> &volumes_and_instances) -{ - if (! m_valid) - return; - - m_mode = mode; - for (unsigned int i : m_list) - (*m_volumes)[i]->selected = false; - m_list.clear(); - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++ i) - if (std::binary_search(volumes_and_instances.begin(), volumes_and_instances.end(), (*m_volumes)[i]->geometry_id)) - do_add_volume(i); - update_type(); - set_bounding_boxes_dirty(); -} - -void Selection::clear() -{ - if (!m_valid) - return; - - if (m_list.empty()) - return; - - // ensure that the volumes get the proper color before next call to render (expecially needed for transparent volumes) - for (unsigned int i : m_list) { - GLVolume& volume = *(*m_volumes)[i]; - volume.selected = false; - volume.set_render_color(volume.color.is_transparent()); - } - - m_list.clear(); - - update_type(); - set_bounding_boxes_dirty(); - - // this happens while the application is closing - if (wxGetApp().obj_manipul() == nullptr) - return; - - // resets the cache in the sidebar - wxGetApp().obj_manipul()->reset_cache(); - - // #et_FIXME fake KillFocus from sidebar - wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event("", false); -} - -// Update the selection based on the new instance IDs. -void Selection::instances_changed(const std::vector &instance_ids_selected) -{ - assert(m_valid); - assert(m_mode == Instance); - m_list.clear(); - for (unsigned int volume_idx = 0; volume_idx < (unsigned int)m_volumes->size(); ++ volume_idx) { - const GLVolume *volume = (*m_volumes)[volume_idx]; - auto it = std::lower_bound(instance_ids_selected.begin(), instance_ids_selected.end(), volume->geometry_id.second); - if (it != instance_ids_selected.end() && *it == volume->geometry_id.second) - this->do_add_volume(volume_idx); - } - update_type(); - this->set_bounding_boxes_dirty(); -} - -// Update the selection based on the map from old indices to new indices after m_volumes changed. -// If the current selection is by instance, this call may select newly added volumes, if they belong to already selected instances. -void Selection::volumes_changed(const std::vector &map_volume_old_to_new) -{ - assert(m_valid); - assert(m_mode == Volume); - IndicesList list_new; - for (unsigned int idx : m_list) - if (map_volume_old_to_new[idx] != size_t(-1)) { - unsigned int new_idx = (unsigned int)map_volume_old_to_new[idx]; - (*m_volumes)[new_idx]->selected = true; - list_new.insert(new_idx); - } - m_list = std::move(list_new); - update_type(); - this->set_bounding_boxes_dirty(); -} - -bool Selection::is_any_connector() const -{ - const int obj_idx = get_object_idx(); - - if ((is_any_volume() || is_any_modifier() || is_mixed()) && // some solid_part AND/OR modifier is selected - obj_idx >= 0 && m_model->objects[obj_idx]->is_cut()) { - const ModelVolumePtrs& obj_volumes = m_model->objects[obj_idx]->volumes; - for (size_t vol_idx = 0; vol_idx < obj_volumes.size(); vol_idx++) - if (obj_volumes[vol_idx]->is_cut_connector()) - for (const GLVolume* v : *m_volumes) - if (v->object_idx() == obj_idx && v->volume_idx() == (int)vol_idx && v->selected) - return true; - } - return false; -} - -bool Selection::is_any_cut_volume() const -{ - const int obj_idx = get_object_idx(); - return is_any_volume() && obj_idx >= 0 && m_model->objects[obj_idx]->is_cut(); -} - -bool Selection::is_single_full_instance() const -{ - if (m_type == SingleFullInstance) - return true; - - if (m_type == SingleFullObject) - return get_instance_idx() != -1; - - if (m_list.empty() || m_volumes->empty()) - return false; - - int object_idx = m_valid ? get_object_idx() : -1; - if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) - return false; - - int instance_idx = (*m_volumes)[*m_list.begin()]->instance_idx(); - - std::set volumes_idxs; - for (unsigned int i : m_list) { - const GLVolume* v = (*m_volumes)[i]; - if (object_idx != v->object_idx() || instance_idx != v->instance_idx()) - return false; - - int volume_idx = v->volume_idx(); - if (volume_idx >= 0) - volumes_idxs.insert(volume_idx); - } - - return m_model->objects[object_idx]->volumes.size() == volumes_idxs.size(); -} - -bool Selection::is_from_single_object() const -{ - const int idx = get_object_idx(); - return 0 <= idx && idx < int(m_model->objects.size()); -} - -bool Selection::is_sla_compliant() const -{ - if (m_mode == Volume) - return false; - - for (unsigned int i : m_list) { - if ((*m_volumes)[i]->is_modifier) - return false; - } - - return true; -} - -bool Selection::contains_all_volumes(const std::vector& volume_idxs) const -{ - for (unsigned int i : volume_idxs) { - if (m_list.find(i) == m_list.end()) - return false; - } - - return true; -} - -bool Selection::contains_any_volume(const std::vector& volume_idxs) const -{ - for (unsigned int i : volume_idxs) { - if (m_list.find(i) != m_list.end()) - return true; - } - - return false; -} - -bool Selection::matches(const std::vector& volume_idxs) const -{ - unsigned int count = 0; - - for (unsigned int i : volume_idxs) { - if (m_list.find(i) != m_list.end()) - ++count; - else - return false; - } - - return count == (unsigned int)m_list.size(); -} - -#if !ENABLE_WORLD_COORDINATE -bool Selection::requires_uniform_scale() const -{ - if (is_single_full_instance() || is_single_modifier() || is_single_volume()) - return false; - - return true; -} -#endif // !ENABLE_WORLD_COORDINATE - -int Selection::get_object_idx() const -{ - return (m_cache.content.size() == 1) ? m_cache.content.begin()->first : -1; -} - -int Selection::get_instance_idx() const -{ - if (m_cache.content.size() == 1) { - const InstanceIdxsList& idxs = m_cache.content.begin()->second; - if (idxs.size() == 1) - return *idxs.begin(); - } - - return -1; -} - -const Selection::InstanceIdxsList& Selection::get_instance_idxs() const -{ - assert(m_cache.content.size() == 1); - return m_cache.content.begin()->second; -} - -const GLVolume* Selection::get_volume(unsigned int volume_idx) const -{ - return (m_valid && (volume_idx < (unsigned int)m_volumes->size())) ? (*m_volumes)[volume_idx] : nullptr; -} - -const BoundingBoxf3& Selection::get_bounding_box() const -{ - if (!m_bounding_box.has_value()) { - std::optional* bbox = const_cast*>(&m_bounding_box); - *bbox = BoundingBoxf3(); - if (m_valid) { - for (unsigned int i : m_list) { - (*bbox)->merge((*m_volumes)[i]->transformed_convex_hull_bounding_box()); - } - } - } - return *m_bounding_box; -} - -const BoundingBoxf3& Selection::get_unscaled_instance_bounding_box() const -{ - assert(is_single_full_instance()); - - if (!m_unscaled_instance_bounding_box.has_value()) { - std::optional* bbox = const_cast*>(&m_unscaled_instance_bounding_box); - *bbox = BoundingBoxf3(); - if (m_valid) { - for (unsigned int i : m_list) { - const GLVolume& volume = *(*m_volumes)[i]; - if (volume.is_modifier) - continue; -#if ENABLE_WORLD_COORDINATE - Transform3d trafo = volume.get_instance_transformation().get_matrix_no_scaling_factor() * volume.get_volume_transformation().get_matrix(); -#else - Transform3d trafo = volume.get_instance_transformation().get_matrix(false, false, true, false) * volume.get_volume_transformation().get_matrix(); -#endif // ENABLE_WORLD_COORDINATE - trafo.translation().z() += volume.get_sla_shift_z(); - (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); - } - } - } - return *m_unscaled_instance_bounding_box; -} - -const BoundingBoxf3& Selection::get_scaled_instance_bounding_box() const -{ - assert(is_single_full_instance()); - - if (!m_scaled_instance_bounding_box.has_value()) { - std::optional* bbox = const_cast*>(&m_scaled_instance_bounding_box); - *bbox = BoundingBoxf3(); - if (m_valid) { - for (unsigned int i : m_list) { - const GLVolume& volume = *(*m_volumes)[i]; - if (volume.is_modifier) - continue; - Transform3d trafo = volume.get_instance_transformation().get_matrix() * volume.get_volume_transformation().get_matrix(); - trafo.translation().z() += volume.get_sla_shift_z(); - (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); - } - } - } - return *m_scaled_instance_bounding_box; -} - -#if ENABLE_WORLD_COORDINATE -const BoundingBoxf3& Selection::get_full_unscaled_instance_bounding_box() const -{ - assert(is_single_full_instance()); - - if (!m_full_unscaled_instance_bounding_box.has_value()) { - std::optional* bbox = const_cast*>(&m_full_unscaled_instance_bounding_box); - *bbox = BoundingBoxf3(); - if (m_valid) { - for (unsigned int i : m_list) { - const GLVolume& volume = *(*m_volumes)[i]; - Transform3d trafo = volume.get_instance_transformation().get_matrix_no_scaling_factor() * volume.get_volume_transformation().get_matrix(); - trafo.translation().z() += volume.get_sla_shift_z(); - (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); - } - } - } - return *m_full_unscaled_instance_bounding_box; -} - -const BoundingBoxf3& Selection::get_full_scaled_instance_bounding_box() const -{ - assert(is_single_full_instance()); - - if (!m_full_scaled_instance_bounding_box.has_value()) { - std::optional* bbox = const_cast*>(&m_full_scaled_instance_bounding_box); - *bbox = BoundingBoxf3(); - if (m_valid) { - for (unsigned int i : m_list) { - const GLVolume& volume = *(*m_volumes)[i]; - Transform3d trafo = volume.get_instance_transformation().get_matrix() * volume.get_volume_transformation().get_matrix(); - trafo.translation().z() += volume.get_sla_shift_z(); - (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); - } - } - } - return *m_full_scaled_instance_bounding_box; -} - -const BoundingBoxf3& Selection::get_full_unscaled_instance_local_bounding_box() const -{ - assert(is_single_full_instance()); - - if (!m_full_unscaled_instance_local_bounding_box.has_value()) { - std::optional* bbox = const_cast*>(&m_full_unscaled_instance_local_bounding_box); - *bbox = BoundingBoxf3(); - if (m_valid) { - for (unsigned int i : m_list) { - const GLVolume& volume = *(*m_volumes)[i]; - Transform3d trafo = volume.get_volume_transformation().get_matrix(); - trafo.translation().z() += volume.get_sla_shift_z(); - (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); - } - } - } - return *m_full_unscaled_instance_local_bounding_box; -} -#endif // ENABLE_WORLD_COORDINATE - -void Selection::setup_cache() -{ - if (!m_valid) - return; - - set_caches(); -} - -#if ENABLE_WORLD_COORDINATE -void Selection::translate(const Vec3d& displacement, TransformationType transformation_type) -{ - if (!m_valid) - return; - - assert(transformation_type.relative()); - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - const VolumeCache& volume_data = m_cache.volumes_data[i]; - if (m_mode == Instance && !is_wipe_tower()) { - assert(is_from_fully_selected_instance(i)); - if (transformation_type.world()) - v.set_instance_transformation(Geometry::translation_transform(displacement) * volume_data.get_instance_full_matrix()); - else if (transformation_type.local()) { - const Vec3d world_displacement = volume_data.get_instance_rotation_matrix() * displacement; - v.set_instance_transformation(Geometry::translation_transform(world_displacement) * volume_data.get_instance_full_matrix()); - } - else - assert(false); - } - else { - const Vec3d offset = transformation_type.local() ? - (Vec3d)(volume_data.get_volume_transform().get_rotation_matrix() * displacement) : displacement; - transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(offset)); - } - } - -#if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) - synchronize_unselected_instances(SyncRotationType::NONE); - else if (m_mode == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - - ensure_not_below_bed(); - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} -#else -void Selection::translate(const Vec3d& displacement, bool local) -{ - if (!m_valid) - return; - - EMode translation_type = m_mode; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - if (m_mode == Volume || v.is_wipe_tower) { - if (local) - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + displacement); - else { - const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); - } - } - else if (m_mode == Instance) { - if (is_from_fully_selected_instance(i)) - v.set_instance_offset(m_cache.volumes_data[i].get_instance_position() + displacement); - else { - const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); - translation_type = Volume; - } - } - } - -#if !DISABLE_INSTANCES_SYNCH - if (translation_type == Instance) - synchronize_unselected_instances(SyncRotationType::NONE); - else if (translation_type == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - - ensure_not_below_bed(); - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} -#endif // ENABLE_WORLD_COORDINATE - -// Rotate an object around one of the axes. Only one rotation component is expected to be changing. -#if ENABLE_WORLD_COORDINATE -void Selection::rotate(const Vec3d& rotation, TransformationType transformation_type) -{ - if (!m_valid) - return; - - assert(transformation_type.relative() || (transformation_type.absolute() && transformation_type.local())); - - const Transform3d rotation_matrix = Geometry::rotation_transform(rotation); - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - const VolumeCache& volume_data = m_cache.volumes_data[i]; - const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); - if (m_mode == Instance && !is_wipe_tower()) { - assert(is_from_fully_selected_instance(i)); - Transform3d new_rotation_matrix = Transform3d::Identity(); - if (transformation_type.absolute()) - new_rotation_matrix = rotation_matrix; - else { - if (transformation_type.world()) - new_rotation_matrix = rotation_matrix * inst_trafo.get_rotation_matrix(); - else if (transformation_type.local()) - new_rotation_matrix = inst_trafo.get_rotation_matrix() * rotation_matrix; - else - assert(false); - } - - const Vec3d new_offset = transformation_type.independent() ? inst_trafo.get_offset() : - m_cache.dragging_center + new_rotation_matrix * inst_trafo.get_rotation_matrix().inverse() * - (inst_trafo.get_offset() - m_cache.dragging_center); - v.set_instance_transformation(Geometry::assemble_transform(Geometry::translation_transform(new_offset), new_rotation_matrix, - inst_trafo.get_scaling_factor_matrix(), inst_trafo.get_mirror_matrix())); - } - else { - if (transformation_type.absolute()) { - const Geometry::Transformation& volume_trafo = volume_data.get_volume_transform(); - v.set_volume_transformation(Geometry::assemble_transform(volume_trafo.get_offset_matrix(), Geometry::rotation_transform(rotation), - volume_trafo.get_scaling_factor_matrix(), volume_trafo.get_mirror_matrix())); - } - else - transform_volume_relative(v, volume_data, transformation_type, Geometry::rotation_transform(rotation)); - } - } - -#if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) { - int rot_axis_max = 0; - rotation.cwiseAbs().maxCoeff(&rot_axis_max); - SyncRotationType synch; - if (transformation_type.world() && rot_axis_max == 2) - synch = SyncRotationType::NONE; - else if (transformation_type.local()) - synch = SyncRotationType::FULL; - else - synch = SyncRotationType::GENERAL; - synchronize_unselected_instances(synch); - } - else if (m_mode == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} -#else -void Selection::rotate(const Vec3d& rotation, TransformationType transformation_type) -{ - if (!m_valid) - return; - - // Only relative rotation values are allowed in the world coordinate system. - assert(!transformation_type.world() || transformation_type.relative()); - - if (!is_wipe_tower()) { - int rot_axis_max = 0; - if (rotation.isApprox(Vec3d::Zero())) { - for (unsigned int i : m_list) { - GLVolume &v = *(*m_volumes)[i]; - if (m_mode == Instance) { - v.set_instance_rotation(m_cache.volumes_data[i].get_instance_rotation()); - v.set_instance_offset(m_cache.volumes_data[i].get_instance_position()); - } - else if (m_mode == Volume) { - v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation()); - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position()); - } - } - } - else { // this is not the wipe tower - //FIXME this does not work for absolute rotations (transformation_type.absolute() is true) - rotation.cwiseAbs().maxCoeff(&rot_axis_max); - -// if ( single instance or single volume ) - // Rotate around center , if only a single object or volume -// transformation_type.set_independent(); - - // For generic rotation, we want to rotate the first volume in selection, and then to synchronize the other volumes with it. - std::vector object_instance_first(m_model->objects.size(), -1); - auto rotate_instance = [this, &rotation, &object_instance_first, rot_axis_max, transformation_type](GLVolume &volume, int i) { - const int first_volume_idx = object_instance_first[volume.object_idx()]; - if (rot_axis_max != 2 && first_volume_idx != -1) { - // Generic rotation, but no rotation around the Z axis. - // Always do a local rotation (do not consider the selection to be a rigid body). - assert(is_approx(rotation.z(), 0.0)); - const GLVolume &first_volume = *(*m_volumes)[first_volume_idx]; - const Vec3d &rotation = first_volume.get_instance_rotation(); - const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[first_volume_idx].get_instance_rotation(), m_cache.volumes_data[i].get_instance_rotation()); - volume.set_instance_rotation(Vec3d(rotation.x(), rotation.y(), rotation.z() + z_diff)); - } - else { - // extracts rotations from the composed transformation - const Vec3d new_rotation = transformation_type.world() ? - Geometry::extract_rotation(Geometry::assemble_transform(Vec3d::Zero(), rotation) * m_cache.volumes_data[i].get_instance_rotation_matrix()) : - transformation_type.absolute() ? rotation : rotation + m_cache.volumes_data[i].get_instance_rotation(); - if (rot_axis_max == 2 && transformation_type.joint()) { - // Only allow rotation of multiple instances as a single rigid body when rotating around the Z axis. - const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), new_rotation); - volume.set_instance_offset(m_cache.dragging_center + Eigen::AngleAxisd(z_diff, Vec3d::UnitZ()) * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); - } - volume.set_instance_rotation(new_rotation); - object_instance_first[volume.object_idx()] = i; - } - }; - - for (unsigned int i : m_list) { - GLVolume &v = *(*m_volumes)[i]; - if (is_single_full_instance()) - rotate_instance(v, i); - else if (is_single_volume() || is_single_modifier()) { - if (transformation_type.independent()) - v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation() + rotation); - else { - const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); - const Vec3d new_rotation = Geometry::extract_rotation(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); - v.set_volume_rotation(new_rotation); - } - } - else { - if (m_mode == Instance) - rotate_instance(v, i); - else if (m_mode == Volume) { - // extracts rotations from the composed transformation - const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); - const Vec3d new_rotation = Geometry::extract_rotation(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); - if (transformation_type.joint()) { - const Vec3d local_pivot = m_cache.volumes_data[i].get_instance_full_matrix().inverse() * m_cache.dragging_center; - const Vec3d offset = m * (m_cache.volumes_data[i].get_volume_position() - local_pivot); - v.set_volume_offset(local_pivot + offset); - } - v.set_volume_rotation(new_rotation); - } - } - } - } - -#if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) - synchronize_unselected_instances((rot_axis_max == 2) ? SyncRotationType::NONE : SyncRotationType::GENERAL); - else if (m_mode == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - } - else { // it's the wipe tower that's selected and being rotated - GLVolume& volume = *((*m_volumes)[*m_list.begin()]); // the wipe tower is always alone in the selection - - // make sure the wipe tower rotates around its center, not origin - // we can assume that only Z rotation changes - const Vec3d center_local = volume.transformed_bounding_box().center() - volume.get_volume_offset(); - const Vec3d center_local_new = Eigen::AngleAxisd(rotation.z()-volume.get_volume_rotation().z(), Vec3d::UnitZ()) * center_local; - volume.set_volume_rotation(rotation); - volume.set_volume_offset(volume.get_volume_offset() + center_local - center_local_new); - } - - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} -#endif // ENABLE_WORLD_COORDINATE - -void Selection::flattening_rotate(const Vec3d& normal) -{ - // We get the normal in untransformed coordinates. We must transform it using the instance matrix, find out - // how to rotate the instance so it faces downwards and do the rotation. All that for all selected instances. - // The function assumes that is_from_single_object() holds. - assert(Slic3r::is_approx(normal.norm(), 1.)); - - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - // Normal transformed from the object coordinate space to the world coordinate space. -#if ENABLE_WORLD_COORDINATE - const Geometry::Transformation& old_inst_trafo = v.get_instance_transformation(); - const Vec3d tnormal = old_inst_trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal; - // Additional rotation to align tnormal with the down vector in the world coordinate space. - const Transform3d rotation_matrix = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ())); - v.set_instance_transformation(old_inst_trafo.get_offset_matrix() * rotation_matrix * old_inst_trafo.get_matrix_no_offset()); -#else - const auto& voldata = m_cache.volumes_data[i]; - Vec3d tnormal = (Geometry::assemble_transform( - Vec3d::Zero(), voldata.get_instance_rotation(), - voldata.get_instance_scaling_factor().cwiseInverse(), voldata.get_instance_mirror()) * normal).normalized(); - // Additional rotation to align tnormal with the down vector in the world coordinate space. - auto extra_rotation = Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()); - v.set_instance_rotation(Geometry::extract_rotation(extra_rotation.toRotationMatrix() * m_cache.volumes_data[i].get_instance_rotation_matrix())); -#endif // ENABLE_WORLD_COORDINATE - } - -#if !DISABLE_INSTANCES_SYNCH - // Apply the same transformation also to other instances, - // but respect their possibly diffrent z-rotation. - if (m_mode == Instance) - synchronize_unselected_instances(SyncRotationType::GENERAL); -#endif // !DISABLE_INSTANCES_SYNCH - - this->set_bounding_boxes_dirty(); -} - -#if ENABLE_WORLD_COORDINATE -void Selection::scale(const Vec3d& scale, TransformationType transformation_type) -{ - scale_and_translate(scale, Vec3d::Zero(), transformation_type); -} -#else -void Selection::scale(const Vec3d& scale, TransformationType transformation_type) -{ - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume &v = *(*m_volumes)[i]; - if (is_single_full_instance()) { - if (transformation_type.relative()) { - const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scale); - const Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_instance_scale_matrix()).matrix().block(0, 0, 3, 3); - // extracts scaling factors from the composed transformation - const Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); - if (transformation_type.joint()) - v.set_instance_offset(m_cache.dragging_center + m * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); - - v.set_instance_scaling_factor(new_scale); - } - else { - if (transformation_type.world() && (std::abs(scale.x() - scale.y()) > EPSILON || std::abs(scale.x() - scale.z()) > EPSILON)) { - // Non-uniform scaling. Transform the scaling factors into the local coordinate system. - // This is only possible, if the instance rotation is mulitples of ninety degrees. - assert(Geometry::is_rotation_ninety_degrees(v.get_instance_rotation())); - v.set_instance_scaling_factor((v.get_instance_transformation().get_matrix(true, false, true, true).matrix().block<3, 3>(0, 0).transpose() * scale).cwiseAbs()); - } - else - v.set_instance_scaling_factor(scale); - } - } - else if (is_single_volume() || is_single_modifier()) - v.set_volume_scaling_factor(scale); - else { - const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scale); - if (m_mode == Instance) { - const Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_instance_scale_matrix()).matrix().block(0, 0, 3, 3); - // extracts scaling factors from the composed transformation - const Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); - if (transformation_type.joint()) - v.set_instance_offset(m_cache.dragging_center + m * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); - - v.set_instance_scaling_factor(new_scale); - } - else if (m_mode == Volume) { - const Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_volume_scale_matrix()).matrix().block(0, 0, 3, 3); - // extracts scaling factors from the composed transformation - const Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); - if (transformation_type.joint()) { - const Vec3d offset = m * (m_cache.volumes_data[i].get_volume_position() + m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center); - v.set_volume_offset(m_cache.dragging_center - m_cache.volumes_data[i].get_instance_position() + offset); - } - v.set_volume_scaling_factor(new_scale); - } - } - } - -#if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) - synchronize_unselected_instances(SyncRotationType::NONE); - else if (m_mode == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - - ensure_on_bed(); - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} -#endif // ENABLE_WORLD_COORDINATE - -void Selection::scale_to_fit_print_volume(const BuildVolume& volume) -{ - auto fit = [this](double s, Vec3d offset) { - if (s <= 0.0 || s == 1.0) - return; - - wxGetApp().plater()->take_snapshot(_L("Scale To Fit")); - - TransformationType type; - type.set_world(); - type.set_relative(); - type.set_joint(); - - // apply scale - setup_cache(); - scale(s * Vec3d::Ones(), type); - wxGetApp().plater()->canvas3D()->do_scale(""); // avoid storing another snapshot - - // center selection on print bed - setup_cache(); - offset.z() = -get_bounding_box().min.z(); -#if ENABLE_WORLD_COORDINATE - TransformationType trafo_type; - trafo_type.set_relative(); - translate(offset, trafo_type); -#else - translate(offset); -#endif // ENABLE_WORLD_COORDINATE - wxGetApp().plater()->canvas3D()->do_move(""); // avoid storing another snapshot - - wxGetApp().obj_manipul()->set_dirty(); - }; - - auto fit_rectangle = [this, fit](const BuildVolume& volume) { - const BoundingBoxf3 print_volume = volume.bounding_volume(); - const Vec3d print_volume_size = print_volume.size(); - - // adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings - const Vec3d box_size = get_bounding_box().size() + 0.02 * Vec3d::Ones(); - - const double sx = (box_size.x() != 0.0) ? print_volume_size.x() / box_size.x() : 0.0; - const double sy = (box_size.y() != 0.0) ? print_volume_size.y() / box_size.y() : 0.0; - const double sz = (box_size.z() != 0.0) ? print_volume_size.z() / box_size.z() : 0.0; - - if (sx != 0.0 && sy != 0.0 && sz != 0.0) - fit(std::min(sx, std::min(sy, sz)), print_volume.center() - get_bounding_box().center()); - }; - - auto fit_circle = [this, fit](const BuildVolume& volume) { - const Geometry::Circled& print_circle = volume.circle(); - double print_circle_radius = unscale(print_circle.radius); - - if (print_circle_radius == 0.0) - return; - - Points points; - double max_z = 0.0; - for (unsigned int i : m_list) { - const GLVolume& v = *(*m_volumes)[i]; - TriangleMesh hull_3d = *v.convex_hull(); - hull_3d.transform(v.world_matrix()); - max_z = std::max(max_z, hull_3d.bounding_box().size().z()); - const Polygon hull_2d = hull_3d.convex_hull(); - points.insert(points.end(), hull_2d.begin(), hull_2d.end()); - } - - if (points.empty()) - return; - - const Geometry::Circled circle = Geometry::smallest_enclosing_circle_welzl(points); - // adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings - const double circle_radius = unscale(circle.radius) + 0.01; - - if (circle_radius == 0.0 || max_z == 0.0) - return; - - const double s = std::min(print_circle_radius / circle_radius, volume.max_print_height() / max_z); - const Vec3d sel_center = get_bounding_box().center(); - const Vec3d offset = s * (Vec3d(unscale(circle.center.x()), unscale(circle.center.y()), 0.5 * max_z) - sel_center); - const Vec3d print_center = { unscale(print_circle.center.x()), unscale(print_circle.center.y()), 0.5 * volume.max_print_height() }; - fit(s, print_center - (sel_center + offset)); - }; - - if (is_empty() || m_mode == Volume) - return; - - switch (volume.type()) - { - case BuildVolume::Type::Rectangle: { fit_rectangle(volume); break; } - case BuildVolume::Type::Circle: { fit_circle(volume); break; } - default: { break; } - } -} - -void Selection::mirror(Axis axis) -{ - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - if (is_single_full_instance()) - v.set_instance_mirror(axis, -v.get_instance_mirror(axis)); - else if (m_mode == Volume) - v.set_volume_mirror(axis, -v.get_volume_mirror(axis)); - } - -#if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) - synchronize_unselected_instances(SyncRotationType::NONE); - else if (m_mode == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - - set_bounding_boxes_dirty(); -} - -#if ENABLE_WORLD_COORDINATE -void Selection::scale_and_translate(const Vec3d& scale, const Vec3d& translation, TransformationType transformation_type) -{ - if (!m_valid) - return; - - Vec3d relative_scale = scale; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - const VolumeCache& volume_data = m_cache.volumes_data[i]; - const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); - - if (transformation_type.absolute()) { - // convert from absolute scaling to relative scaling - BoundingBoxf3 original_box; - if (m_mode == Instance) { - assert(is_from_fully_selected_instance(i)); - if (transformation_type.world()) - original_box = get_full_unscaled_instance_bounding_box(); - else - original_box = get_full_unscaled_instance_local_bounding_box(); - } - else { - if (transformation_type.world()) - original_box = v.transformed_convex_hull_bounding_box((volume_data.get_instance_transform() * - volume_data.get_volume_transform()).get_matrix_no_scaling_factor()); - else if (transformation_type.instance()) - original_box = v.transformed_convex_hull_bounding_box(volume_data.get_volume_transform().get_matrix_no_scaling_factor()); - else - original_box = v.bounding_box(); - } - - relative_scale = original_box.size().cwiseProduct(scale).cwiseQuotient(m_box.get_bounding_box().size()); - } - - if (m_mode == Instance) { - assert(is_from_fully_selected_instance(i)); - if (transformation_type.world()) { - const Transform3d scale_matrix = Geometry::scale_transform(relative_scale); - const Transform3d offset_matrix = (transformation_type.joint() && translation.isApprox(Vec3d::Zero())) ? - // non-constrained scaling - add offset to scale around selection center - Geometry::translation_transform(m_cache.dragging_center + scale_matrix * (inst_trafo.get_offset() - m_cache.dragging_center)) : - // constrained scaling - add offset to keep constraint - Geometry::translation_transform(translation) * inst_trafo.get_offset_matrix(); - v.set_instance_transformation(offset_matrix * scale_matrix * inst_trafo.get_matrix_no_offset()); - } - else if (transformation_type.local()) { - const Transform3d scale_matrix = Geometry::scale_transform(relative_scale); - Vec3d offset; - if (transformation_type.joint() && translation.isApprox(Vec3d::Zero())) { - // non-constrained scaling - add offset to scale around selection center - offset = inst_trafo.get_matrix_no_offset().inverse() * (inst_trafo.get_offset() - m_cache.dragging_center); - offset = inst_trafo.get_matrix_no_offset() * (scale_matrix * offset - offset); - } - else - // constrained scaling - add offset to keep constraint - offset = translation; - - v.set_instance_transformation(Geometry::translation_transform(offset) * inst_trafo.get_matrix() * scale_matrix); - } - else - assert(false); - } - else - transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(relative_scale)); - } - -#if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) - synchronize_unselected_instances(SyncRotationType::NONE); - else if (m_mode == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - - ensure_on_bed(); - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} - -void Selection::reset_skew() -{ - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - const VolumeCache& volume_data = m_cache.volumes_data[i]; - Geometry::Transformation inst_trafo = volume_data.get_instance_transform(); - Geometry::Transformation vol_trafo = volume_data.get_volume_transform(); - Geometry::Transformation world_trafo = inst_trafo * vol_trafo; - if (world_trafo.has_skew()) { - if (!inst_trafo.has_skew() && !vol_trafo.has_skew()) { - // = [I][V] - world_trafo.reset_offset(); - world_trafo.reset_skew(); - v.set_volume_transformation(vol_trafo.get_offset_matrix() * inst_trafo.get_matrix_no_offset().inverse() * world_trafo.get_matrix()); - } - else { - // = - // = [V] - // = [I] - if (inst_trafo.has_skew()) { - inst_trafo.reset_skew(); - v.set_instance_transformation(inst_trafo); - } - if (vol_trafo.has_skew()) { - vol_trafo.reset_skew(); - v.set_volume_transformation(vol_trafo); - } - } - } - else { - // [W] = [I][V] - // [W] = - if (inst_trafo.has_skew()) { - inst_trafo.reset_skew(); - v.set_instance_transformation(inst_trafo); - } - if (vol_trafo.has_skew()) { - vol_trafo.reset_skew(); - v.set_volume_transformation(vol_trafo); - } - } - } - - ensure_on_bed(); - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} -#else -void Selection::translate(unsigned int object_idx, const Vec3d& displacement) -{ - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - if (v.object_idx() == (int)object_idx) - v.set_instance_offset(v.get_instance_offset() + displacement); - } - - std::set done; // prevent processing volumes twice - done.insert(m_list.begin(), m_list.end()); - - for (unsigned int i : m_list) { - if (done.size() == m_volumes->size()) - break; - - if ((*m_volumes)[i]->is_wipe_tower) - continue; - - int object_idx = (*m_volumes)[i]->object_idx(); - - // Process unselected volumes of the object. - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (done.size() == m_volumes->size()) - break; - - if (done.find(j) != done.end()) - continue; - - GLVolume& v = *(*m_volumes)[j]; - if (v.object_idx() != object_idx) - continue; - - v.set_instance_offset(v.get_instance_offset() + displacement); - done.insert(j); - } - } - - this->set_bounding_boxes_dirty(); -} -#endif // ENABLE_WORLD_COORDINATE - -void Selection::translate(unsigned int object_idx, unsigned int instance_idx, const Vec3d& displacement) -{ - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - if (v.object_idx() == (int)object_idx && v.instance_idx() == (int)instance_idx) -#if ENABLE_WORLD_COORDINATE - v.set_instance_transformation(Geometry::translation_transform(displacement) * v.get_instance_transformation().get_matrix()); -#else - v.set_instance_offset(v.get_instance_offset() + displacement); -#endif // ENABLE_WORLD_COORDINATE - } - - std::set done; // prevent processing volumes twice - done.insert(m_list.begin(), m_list.end()); - - for (unsigned int i : m_list) { - if (done.size() == m_volumes->size()) - break; - - if ((*m_volumes)[i]->is_wipe_tower) - continue; - - const int object_idx = (*m_volumes)[i]->object_idx(); - - // Process unselected volumes of the object. - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (done.size() == m_volumes->size()) - break; - - if (done.find(j) != done.end()) - continue; - - GLVolume& v = *(*m_volumes)[j]; - if (v.object_idx() != object_idx || v.instance_idx() != (int)instance_idx) - continue; - -#if ENABLE_WORLD_COORDINATE - v.set_instance_transformation(Geometry::translation_transform(displacement) * v.get_instance_transformation().get_matrix()); -#else - v.set_instance_offset(v.get_instance_offset() + displacement); -#endif // ENABLE_WORLD_COORDINATE - done.insert(j); - } - } - - this->set_bounding_boxes_dirty(); -} - -#if ENABLE_WORLD_COORDINATE -int Selection::bake_transform_if_needed() const -{ - if ((is_single_full_instance() && wxGetApp().obj_manipul()->is_world_coordinates()) || - (is_single_volume_or_modifier() && !wxGetApp().obj_manipul()->is_local_coordinates())) { - // Verify whether the instance rotation is multiples of 90 degrees, so that the scaling in world coordinates is possible. - // all volumes in the selection belongs to the same instance, any of them contains the needed instance data, so we take the first one - const GLVolume& volume = *get_first_volume(); - bool needs_baking = false; - if (is_single_full_instance()) { - // Is the instance angle close to a multiple of 90 degrees? - needs_baking |= !Geometry::is_rotation_ninety_degrees(volume.get_instance_rotation()); - // Are all volumes angles close to a multiple of 90 degrees? - for (unsigned int id : get_volume_idxs()) { - if (needs_baking) - break; - needs_baking |= !Geometry::is_rotation_ninety_degrees(get_volume(id)->get_volume_rotation()); - } - } - else if (is_single_volume_or_modifier()) { - // is the volume angle close to a multiple of 90 degrees? - needs_baking |= !Geometry::is_rotation_ninety_degrees(volume.get_volume_rotation()); - if (wxGetApp().obj_manipul()->is_world_coordinates()) - // Is the instance angle close to a multiple of 90 degrees? - needs_baking |= !Geometry::is_rotation_ninety_degrees(volume.get_instance_rotation()); - } - - if (needs_baking) { - MessageDialog dlg((wxWindow*)wxGetApp().mainframe, - _L("The currently manipulated object is tilted or contains tilted parts (rotation angles are not multiples of 90°). " - "Non-uniform scaling of tilted objects is only possible in non-local coordinate systems, " - "once the rotation is embedded into the object coordinates.") + "\n" + - _L("This operation is irreversible.") + "\n" + - _L("Do you want to proceed?"), - SLIC3R_APP_NAME, - wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION); - if (dlg.ShowModal() != wxID_YES) - return -1; - - wxGetApp().plater()->take_snapshot(_("Bake transform")); - - // Bake the rotation into the meshes of the object. - wxGetApp().model().objects[volume.composite_id.object_id]->bake_xy_rotation_into_meshes(volume.composite_id.instance_id); - // Update the 3D scene, selections etc. - wxGetApp().plater()->update(); - return 0; - } - } - - return 1; -} -#endif // ENABLE_WORLD_COORDINATE - -void Selection::erase() -{ - if (!m_valid) - return; - - if (is_single_full_object()) - wxGetApp().obj_list()->delete_from_model_and_list(ItemType::itObject, get_object_idx(), 0); - else if (is_multiple_full_object()) { - std::vector items; - items.reserve(m_cache.content.size()); - for (ObjectIdxsToInstanceIdxsMap::iterator it = m_cache.content.begin(); it != m_cache.content.end(); ++it) { - items.emplace_back(ItemType::itObject, it->first, 0); - } - wxGetApp().obj_list()->delete_from_model_and_list(items); - } - else if (is_multiple_full_instance()) { - std::set> instances_idxs; - for (ObjectIdxsToInstanceIdxsMap::iterator obj_it = m_cache.content.begin(); obj_it != m_cache.content.end(); ++obj_it) { - for (InstanceIdxsList::reverse_iterator inst_it = obj_it->second.rbegin(); inst_it != obj_it->second.rend(); ++inst_it) { - instances_idxs.insert(std::make_pair(obj_it->first, *inst_it)); - } - } - - std::vector items; - items.reserve(instances_idxs.size()); - for (const std::pair& i : instances_idxs) { - items.emplace_back(ItemType::itInstance, i.first, i.second); - } - wxGetApp().obj_list()->delete_from_model_and_list(items); - } - else if (is_single_full_instance()) - wxGetApp().obj_list()->delete_from_model_and_list(ItemType::itInstance, get_object_idx(), get_instance_idx()); - else if (is_mixed()) { - std::set items_set; - std::map volumes_in_obj; - - for (auto i : m_list) { - const auto gl_vol = (*m_volumes)[i]; - const auto glv_obj_idx = gl_vol->object_idx(); - const auto model_object = m_model->objects[glv_obj_idx]; - - if (model_object->instances.size() == 1) { - if (model_object->volumes.size() == 1) - items_set.insert(ItemForDelete(ItemType::itObject, glv_obj_idx, -1)); - else { - items_set.insert(ItemForDelete(ItemType::itVolume, glv_obj_idx, gl_vol->volume_idx())); - int idx = (volumes_in_obj.find(glv_obj_idx) == volumes_in_obj.end()) ? 0 : volumes_in_obj.at(glv_obj_idx); - volumes_in_obj[glv_obj_idx] = ++idx; - } - continue; - } - - const auto glv_ins_idx = gl_vol->instance_idx(); - - for (auto obj_ins : m_cache.content) { - if (obj_ins.first == glv_obj_idx) { - if (obj_ins.second.find(glv_ins_idx) != obj_ins.second.end()) { - if (obj_ins.second.size() == model_object->instances.size()) - items_set.insert(ItemForDelete(ItemType::itObject, glv_obj_idx, -1)); - else - items_set.insert(ItemForDelete(ItemType::itInstance, glv_obj_idx, glv_ins_idx)); - - break; - } - } - } - } - - std::vector items; - items.reserve(items_set.size()); - for (const ItemForDelete& i : items_set) { - if (i.type == ItemType::itVolume) { - const int vol_in_obj_cnt = volumes_in_obj.find(i.obj_idx) == volumes_in_obj.end() ? 0 : volumes_in_obj.at(i.obj_idx); - if (vol_in_obj_cnt == (int)m_model->objects[i.obj_idx]->volumes.size()) { - if (i.sub_obj_idx == vol_in_obj_cnt - 1) - items.emplace_back(ItemType::itObject, i.obj_idx, 0); - continue; - } - } - items.emplace_back(i.type, i.obj_idx, i.sub_obj_idx); - } - - wxGetApp().obj_list()->delete_from_model_and_list(items); - } - else { - std::set> volumes_idxs; - for (unsigned int i : m_list) { - const GLVolume* v = (*m_volumes)[i]; - // Only remove volumes associated with ModelVolumes from the object list. - // Temporary meshes (SLA supports or pads) are not managed by the object list. - if (v->volume_idx() >= 0) - volumes_idxs.insert(std::make_pair(v->object_idx(), v->volume_idx())); - } - - std::vector items; - items.reserve(volumes_idxs.size()); - for (const std::pair& v : volumes_idxs) { - items.emplace_back(ItemType::itVolume, v.first, v.second); - } - - wxGetApp().obj_list()->delete_from_model_and_list(items); - ensure_not_below_bed(); - } -} - -void Selection::render(float scale_factor) -{ - if (!m_valid || is_empty()) - return; - - m_scale_factor = scale_factor; - // render cumulative bounding box of selected volumes -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_WORLD_COORDINATE - BoundingBoxf3 box; - Transform3d trafo; - const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - if (coordinates_type == ECoordinatesType::World) { - box = get_bounding_box(); - trafo = Transform3d::Identity(); - } - else if (coordinates_type == ECoordinatesType::Local && is_single_volume_or_modifier()) { - const GLVolume& v = *get_first_volume(); - box = v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_scaling_factor_matrix()); - trafo = v.get_instance_transformation().get_matrix() * v.get_volume_transformation().get_matrix_no_scaling_factor(); - } - else { - const Selection::IndicesList& ids = get_volume_idxs(); - for (unsigned int id : ids) { - const GLVolume& v = *get_volume(id); - box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); - } - const Geometry::Transformation inst_trafo = get_first_volume()->get_instance_transformation(); - box = box.transformed(inst_trafo.get_scaling_factor_matrix()); - trafo = inst_trafo.get_matrix_no_scaling_factor(); - } - - render_bounding_box(box, trafo, ColorRGB::WHITE()); -#else - render_bounding_box(get_bounding_box(), ColorRGB::WHITE()); -#endif // ENABLE_WORLD_COORDINATE -#else - render_selected_volumes(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - render_synchronized_volumes(); -} - -#if ENABLE_RENDER_SELECTION_CENTER -void Selection::render_center(bool gizmo_is_dragging) -{ - if (!m_valid || is_empty()) - return; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - shader->start_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - const Vec3d center = gizmo_is_dragging ? m_cache.dragging_center : get_bounding_box().center(); - - glsafe(::glDisable(GL_DEPTH_TEST)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - Transform3d view_model_matrix = camera.get_view_matrix() * Geometry::assemble_transform(center); - - shader->set_uniform("view_model_matrix", view_model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - m_vbo_sphere.set_color(ColorRGBA::WHITE()); -#else - glsafe(::glPushMatrix()); - glsafe(::glTranslated(center.x(), center.y(), center.z())); - m_vbo_sphere.set_color(-1, ColorRGBA::WHITE()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - m_vbo_sphere.render(); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); -#else - glsafe(::glPopMatrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} -#endif // ENABLE_RENDER_SELECTION_CENTER - -void Selection::render_sidebar_hints(const std::string& sidebar_field) -{ - if (sidebar_field.empty()) - return; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader(boost::starts_with(sidebar_field, "layer") ? "flat" : "gouraud_light"); - if (shader == nullptr) - return; - - shader->start_using(); -#else - GLShaderProgram* shader = nullptr; - - if (!boost::starts_with(sidebar_field, "layer")) { - shader = wxGetApp().get_shader("gouraud_light"); - if (shader == nullptr) - return; - - shader->start_using(); - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glEnable(GL_DEPTH_TEST)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Transform3d base_matrix = Geometry::translation_transform(get_bounding_box().center()); - Transform3d orient_matrix = Transform3d::Identity(); -#else - glsafe(::glPushMatrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE - const Vec3d center = get_bounding_box().center(); - Vec3d axes_center = center; -#endif // ENABLE_WORLD_COORDINATE - - if (!boost::starts_with(sidebar_field, "layer")) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->set_uniform("emission_factor", 0.05f); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#if !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE - const Vec3d& center = get_bounding_box().center(); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE -#if ENABLE_WORLD_COORDINATE - if (is_single_full_instance() && !wxGetApp().obj_manipul()->is_world_coordinates()) { -#else - if (is_single_full_instance() && !wxGetApp().obj_manipul()->get_world_coordinates()) { -#endif // ENABLE_WORLD_COORDINATE -#if !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE - glsafe(::glTranslated(center.x(), center.y(), center.z())); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE -#if ENABLE_WORLD_COORDINATE - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); - axes_center = (*m_volumes)[*m_list.begin()]->get_instance_offset(); -#else - if (!boost::starts_with(sidebar_field, "position")) { -#if !ENABLE_LEGACY_OPENGL_REMOVAL - Transform3d orient_matrix = Transform3d::Identity(); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - if (boost::starts_with(sidebar_field, "scale")) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); - else if (boost::starts_with(sidebar_field, "rotation")) { - if (boost::ends_with(sidebar_field, "x")) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); - else if (boost::ends_with(sidebar_field, "y")) { - const Vec3d& rotation = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation(); - if (rotation.x() == 0.0) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); - else - orient_matrix.rotate(Eigen::AngleAxisd(rotation.z(), Vec3d::UnitZ())); - } - } -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glMultMatrixd(orient_matrix.data())); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - } -#endif // ENABLE_WORLD_COORDINATE - } -#if ENABLE_WORLD_COORDINATE - else if (is_single_volume_or_modifier()) { -#else - else if (is_single_volume() || is_single_modifier()) { -#endif // ENABLE_WORLD_COORDINATE -#if !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE - glsafe(::glTranslated(center.x(), center.y(), center.z())); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE -#if ENABLE_WORLD_COORDINATE - if (!wxGetApp().obj_manipul()->is_world_coordinates()) { - if (wxGetApp().obj_manipul()->is_local_coordinates()) { - const GLVolume* v = (*m_volumes)[*m_list.begin()]; - orient_matrix = v->get_instance_transformation().get_rotation_matrix() * v->get_volume_transformation().get_rotation_matrix(); - axes_center = (*m_volumes)[*m_list.begin()]->world_matrix().translation(); - } - else { - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); - axes_center = (*m_volumes)[*m_list.begin()]->get_instance_offset(); - } - } -#else -#if ENABLE_LEGACY_OPENGL_REMOVAL - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); -#else - glsafe(::glTranslated(center.x(), center.y(), center.z())); - Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - if (!boost::starts_with(sidebar_field, "position")) - orient_matrix = orient_matrix * (*m_volumes)[*m_list.begin()]->get_volume_transformation().get_matrix(true, false, true, true); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glMultMatrixd(orient_matrix.data())); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -#endif // ENABLE_WORLD_COORDINATE - } - else { -#if ENABLE_LEGACY_OPENGL_REMOVAL|| ENABLE_WORLD_COORDINATE - if (requires_local_axes()) -#if ENABLE_WORLD_COORDINATE - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); -#else - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); -#endif // ENABLE_WORLD_COORDINATE -#else - glsafe(::glTranslated(center.x(), center.y(), center.z())); - if (requires_local_axes()) { - const Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); - glsafe(::glMultMatrixd(orient_matrix.data())); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL|| ENABLE_WORLD_COORDINATE - } - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!boost::starts_with(sidebar_field, "layer")) - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE - if (!boost::starts_with(sidebar_field, "layer")) { - shader->set_uniform("emission_factor", 0.1f); -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPushMatrix()); - glsafe(::glTranslated(center.x(), center.y(), center.z())); - glsafe(::glMultMatrixd(orient_matrix.data())); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - } -#endif // ENABLE_WORLD_COORDINATE - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (boost::starts_with(sidebar_field, "position")) - render_sidebar_position_hints(sidebar_field, *shader, base_matrix * orient_matrix); - else if (boost::starts_with(sidebar_field, "rotation")) - render_sidebar_rotation_hints(sidebar_field, *shader, base_matrix * orient_matrix); - else if (boost::starts_with(sidebar_field, "scale") || boost::starts_with(sidebar_field, "size")) - render_sidebar_scale_hints(sidebar_field, *shader, base_matrix * orient_matrix); - else if (boost::starts_with(sidebar_field, "layer")) - render_sidebar_layers_hints(sidebar_field, *shader); - -#if ENABLE_WORLD_COORDINATE - if (!boost::starts_with(sidebar_field, "layer")) { - if (!wxGetApp().obj_manipul()->is_world_coordinates()) - m_axes.render(Geometry::translation_transform(axes_center) * orient_matrix, 0.25f); - } -#endif // ENABLE_WORLD_COORDINATE -#else - if (boost::starts_with(sidebar_field, "position")) - render_sidebar_position_hints(sidebar_field); - else if (boost::starts_with(sidebar_field, "rotation")) - render_sidebar_rotation_hints(sidebar_field); - else if (boost::starts_with(sidebar_field, "scale") || boost::starts_with(sidebar_field, "size")) - render_sidebar_scale_hints(sidebar_field); - else if (boost::starts_with(sidebar_field, "layer")) - render_sidebar_layers_hints(sidebar_field); - -#if ENABLE_WORLD_COORDINATE - if (!boost::starts_with(sidebar_field, "layer")) { - glsafe(::glPopMatrix()); - glsafe(::glPushMatrix()); - glsafe(::glTranslated(axes_center.x(), axes_center.y(), axes_center.z())); - glsafe(::glMultMatrixd(orient_matrix.data())); - if (!wxGetApp().obj_manipul()->is_world_coordinates()) - m_axes.render(0.25f); - glsafe(::glPopMatrix()); - } -#endif // ENABLE_WORLD_COORDINATE -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -#endif // ENABLE_WORLD_COORDINATE - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - if (!boost::starts_with(sidebar_field, "layer")) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); -} - -bool Selection::requires_local_axes() const -{ - return m_mode == Volume && is_from_single_instance(); -} - -void Selection::copy_to_clipboard() -{ - if (!m_valid) - return; - - m_clipboard.reset(); - - for (const ObjectIdxsToInstanceIdxsMap::value_type& object : m_cache.content) { - ModelObject* src_object = m_model->objects[object.first]; - ModelObject* dst_object = m_clipboard.add_object(); - dst_object->name = src_object->name; - dst_object->input_file = src_object->input_file; - dst_object->config.assign_config(src_object->config); - dst_object->sla_support_points = src_object->sla_support_points; - dst_object->sla_points_status = src_object->sla_points_status; - dst_object->sla_drain_holes = src_object->sla_drain_holes; - dst_object->layer_config_ranges = src_object->layer_config_ranges; // #ys_FIXME_experiment - dst_object->layer_height_profile.assign(src_object->layer_height_profile); - dst_object->origin_translation = src_object->origin_translation; - - for (int i : object.second) { - dst_object->add_instance(*src_object->instances[i]); - } - - for (unsigned int i : m_list) { - // Copy the ModelVolumes only for the selected GLVolumes of the 1st selected instance. - const GLVolume* volume = (*m_volumes)[i]; - if (volume->object_idx() == object.first && volume->instance_idx() == *object.second.begin()) { - int volume_idx = volume->volume_idx(); - if (0 <= volume_idx && volume_idx < (int)src_object->volumes.size()) { - ModelVolume* src_volume = src_object->volumes[volume_idx]; - ModelVolume* dst_volume = dst_object->add_volume(*src_volume); - dst_volume->set_new_unique_id(); - } - else - assert(false); - } - } - } - - m_clipboard.set_mode(m_mode); -} - -void Selection::paste_from_clipboard() -{ - if (!m_valid || m_clipboard.is_empty()) - return; - - switch (m_clipboard.get_mode()) - { - case Volume: - { - if (is_from_single_instance()) - paste_volumes_from_clipboard(); - - break; - } - case Instance: - { - if (m_mode == Instance) - paste_objects_from_clipboard(); - - break; - } - } -} - -std::vector Selection::get_volume_idxs_from_object(unsigned int object_idx) const -{ - std::vector idxs; - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - if ((*m_volumes)[i]->object_idx() == (int)object_idx) - idxs.push_back(i); - } - - return idxs; -} - -std::vector Selection::get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const -{ - std::vector idxs; - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - const GLVolume* v = (*m_volumes)[i]; - if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx) - idxs.push_back(i); - } - - return idxs; -} - -std::vector Selection::get_volume_idxs_from_volume(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx) const -{ - std::vector idxs; - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) - { - const GLVolume* v = (*m_volumes)[i]; - if (v->object_idx() == (int)object_idx && v->volume_idx() == (int)volume_idx) { - if ((int)instance_idx != -1 && v->instance_idx() == (int)instance_idx) - idxs.push_back(i); - } - } - - return idxs; -} - -std::vector Selection::get_missing_volume_idxs_from(const std::vector& volume_idxs) const -{ - std::vector idxs; - - for (unsigned int i : m_list) { - std::vector::const_iterator it = std::find(volume_idxs.begin(), volume_idxs.end(), i); - if (it == volume_idxs.end()) - idxs.push_back(i); - } - - return idxs; -} - -std::vector Selection::get_unselected_volume_idxs_from(const std::vector& volume_idxs) const -{ - std::vector idxs; - - for (unsigned int i : volume_idxs) { - if (m_list.find(i) == m_list.end()) - idxs.push_back(i); - } - - return idxs; -} - -void Selection::update_valid() -{ - m_valid = (m_volumes != nullptr) && (m_model != nullptr); -} - -void Selection::update_type() -{ - m_cache.content.clear(); - m_type = Mixed; - - for (unsigned int i : m_list) { - const GLVolume* volume = (*m_volumes)[i]; - int obj_idx = volume->object_idx(); - int inst_idx = volume->instance_idx(); - ObjectIdxsToInstanceIdxsMap::iterator obj_it = m_cache.content.find(obj_idx); - if (obj_it == m_cache.content.end()) - obj_it = m_cache.content.insert(ObjectIdxsToInstanceIdxsMap::value_type(obj_idx, InstanceIdxsList())).first; - - obj_it->second.insert(inst_idx); - } - - bool requires_disable = false; - - if (!m_valid) - m_type = Invalid; - else - { - if (m_list.empty()) - m_type = Empty; - else if (m_list.size() == 1) { - const GLVolume* first = (*m_volumes)[*m_list.begin()]; - if (first->is_wipe_tower) - m_type = WipeTower; - else if (first->is_modifier) { - m_type = SingleModifier; - requires_disable = true; - } - else { - const ModelObject* model_object = m_model->objects[first->object_idx()]; - unsigned int volumes_count = (unsigned int)model_object->volumes.size(); - unsigned int instances_count = (unsigned int)model_object->instances.size(); - if (volumes_count * instances_count == 1) { - m_type = SingleFullObject; - // ensures the correct mode is selected - m_mode = Instance; - } - else if (volumes_count == 1) // instances_count > 1 - { - m_type = SingleFullInstance; - // ensures the correct mode is selected - m_mode = Instance; - } - else { - m_type = SingleVolume; - requires_disable = true; - } - } - } - else { - unsigned int sla_volumes_count = 0; - // Note: sla_volumes_count is a count of the selected sla_volumes per object instead of per instance, like a model_volumes_count is - for (unsigned int i : m_list) { - if ((*m_volumes)[i]->volume_idx() < 0) - ++sla_volumes_count; - } - - if (m_cache.content.size() == 1) // single object - { - const ModelObject* model_object = m_model->objects[m_cache.content.begin()->first]; - unsigned int model_volumes_count = (unsigned int)model_object->volumes.size(); - - unsigned int instances_count = (unsigned int)model_object->instances.size(); - unsigned int selected_instances_count = (unsigned int)m_cache.content.begin()->second.size(); - if (model_volumes_count * instances_count + sla_volumes_count == (unsigned int)m_list.size()) { - m_type = SingleFullObject; - // ensures the correct mode is selected - m_mode = Instance; - } - else if (selected_instances_count == 1) { - if (model_volumes_count + sla_volumes_count == (unsigned int)m_list.size()) { - m_type = SingleFullInstance; - // ensures the correct mode is selected - m_mode = Instance; - } - else { - unsigned int modifiers_count = 0; - for (unsigned int i : m_list) { - if ((*m_volumes)[i]->is_modifier) - ++modifiers_count; - } - - if (modifiers_count == 0) - m_type = MultipleVolume; - else if (modifiers_count == (unsigned int)m_list.size()) - m_type = MultipleModifier; - - requires_disable = true; - } - } - else if (selected_instances_count > 1 && selected_instances_count * model_volumes_count + sla_volumes_count == (unsigned int)m_list.size()) { - m_type = MultipleFullInstance; - // ensures the correct mode is selected - m_mode = Instance; - } - } - else { - unsigned int sels_cntr = 0; - for (ObjectIdxsToInstanceIdxsMap::iterator it = m_cache.content.begin(); it != m_cache.content.end(); ++it) { - const ModelObject* model_object = m_model->objects[it->first]; - unsigned int volumes_count = (unsigned int)model_object->volumes.size(); - unsigned int instances_count = (unsigned int)model_object->instances.size(); - sels_cntr += volumes_count * instances_count; - } - if (sels_cntr + sla_volumes_count == (unsigned int)m_list.size()) { - m_type = MultipleFullObject; - // ensures the correct mode is selected - m_mode = Instance; - } - } - } - } - - int object_idx = get_object_idx(); - int instance_idx = get_instance_idx(); - for (GLVolume* v : *m_volumes) { - v->disabled = requires_disable ? (v->object_idx() != object_idx) || (v->instance_idx() != instance_idx) : false; - } - -#if ENABLE_SELECTION_DEBUG_OUTPUT - std::cout << "Selection: "; - std::cout << "mode: "; - switch (m_mode) - { - case Volume: - { - std::cout << "Volume"; - break; - } - case Instance: - { - std::cout << "Instance"; - break; - } - } - - std::cout << " - type: "; - - switch (m_type) - { - case Invalid: - { - std::cout << "Invalid" << std::endl; - break; - } - case Empty: - { - std::cout << "Empty" << std::endl; - break; - } - case WipeTower: - { - std::cout << "WipeTower" << std::endl; - break; - } - case SingleModifier: - { - std::cout << "SingleModifier" << std::endl; - break; - } - case MultipleModifier: - { - std::cout << "MultipleModifier" << std::endl; - break; - } - case SingleVolume: - { - std::cout << "SingleVolume" << std::endl; - break; - } - case MultipleVolume: - { - std::cout << "MultipleVolume" << std::endl; - break; - } - case SingleFullObject: - { - std::cout << "SingleFullObject" << std::endl; - break; - } - case MultipleFullObject: - { - std::cout << "MultipleFullObject" << std::endl; - break; - } - case SingleFullInstance: - { - std::cout << "SingleFullInstance" << std::endl; - break; - } - case MultipleFullInstance: - { - std::cout << "MultipleFullInstance" << std::endl; - break; - } - case Mixed: - { - std::cout << "Mixed" << std::endl; - break; - } - } -#endif // ENABLE_SELECTION_DEBUG_OUTPUT -} - -void Selection::set_caches() -{ - m_cache.volumes_data.clear(); - m_cache.sinking_volumes.clear(); - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - const GLVolume& v = *(*m_volumes)[i]; - m_cache.volumes_data.emplace(i, VolumeCache(v.get_volume_transformation(), v.get_instance_transformation())); - if (v.is_sinking()) - m_cache.sinking_volumes.push_back(i); - } - m_cache.dragging_center = get_bounding_box().center(); -} - -void Selection::do_add_volume(unsigned int volume_idx) -{ - m_list.insert(volume_idx); - GLVolume* v = (*m_volumes)[volume_idx]; - v->selected = true; - if (v->hover == GLVolume::HS_Select || v->hover == GLVolume::HS_Deselect) - v->hover = GLVolume::HS_Hover; -} - -void Selection::do_add_volumes(const std::vector& volume_idxs) -{ - for (unsigned int i : volume_idxs) - { - if (i < (unsigned int)m_volumes->size()) - do_add_volume(i); - } -} - -void Selection::do_remove_volume(unsigned int volume_idx) -{ - IndicesList::iterator v_it = m_list.find(volume_idx); - if (v_it == m_list.end()) - return; - - m_list.erase(v_it); - - (*m_volumes)[volume_idx]->selected = false; -} - -void Selection::do_remove_instance(unsigned int object_idx, unsigned int instance_idx) -{ - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - GLVolume* v = (*m_volumes)[i]; - if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx) - do_remove_volume(i); - } -} - -void Selection::do_remove_object(unsigned int object_idx) -{ - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - GLVolume* v = (*m_volumes)[i]; - if (v->object_idx() == (int)object_idx) - do_remove_volume(i); - } -} - -#if !ENABLE_LEGACY_OPENGL_REMOVAL -void Selection::render_selected_volumes() const -{ - float color[3] = { 1.0f, 1.0f, 1.0f }; - render_bounding_box(get_bounding_box(), color); -} -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -void Selection::render_synchronized_volumes() -{ - if (m_mode == Instance) - return; - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - float color[3] = { 1.0f, 1.0f, 0.0f }; -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_WORLD_COORDINATE - const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); - BoundingBoxf3 box; - Transform3d trafo; -#endif // ENABLE_WORLD_COORDINATE - - for (unsigned int i : m_list) { - const GLVolume& volume = *(*m_volumes)[i]; - int object_idx = volume.object_idx(); - int volume_idx = volume.volume_idx(); - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (i == j) - continue; - - const GLVolume& v = *(*m_volumes)[j]; - if (v.object_idx() != object_idx || v.volume_idx() != volume_idx) - continue; - -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_WORLD_COORDINATE - if (coordinates_type == ECoordinatesType::World) { - box = v.transformed_convex_hull_bounding_box(); - trafo = Transform3d::Identity(); - } - else if (coordinates_type == ECoordinatesType::Local) { - box = v.bounding_box(); - trafo = v.world_matrix(); - } - else { - box = v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix()); - trafo = v.get_instance_transformation().get_matrix(); - } - render_bounding_box(box, trafo, ColorRGB::YELLOW()); -#else - render_bounding_box(v.transformed_convex_hull_bounding_box(), ColorRGB::YELLOW()); -#endif // ENABLE_WORLD_COORDINATE -#else - render_bounding_box(v.transformed_convex_hull_bounding_box(), color); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_WORLD_COORDINATE -void Selection::render_bounding_box(const BoundingBoxf3& box, const Transform3d& trafo, const ColorRGB& color) -#else -void Selection::render_bounding_box(const BoundingBoxf3& box, const ColorRGB& color) -#endif // ENABLE_WORLD_COORDINATE -{ -#else -void Selection::render_bounding_box(const BoundingBoxf3 & box, float* color) const -{ - if (color == nullptr) - return; - - const Vec3f b_min = box.min.cast(); - const Vec3f b_max = box.max.cast(); - const Vec3f size = 0.2f * box.size().cast(); - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glColor3fv(color)); - glsafe(::glLineWidth(2.0f * m_scale_factor)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const BoundingBoxf3& curr_box = m_box.get_bounding_box(); - - if (!m_box.is_initialized() || !is_approx(box.min, curr_box.min) || !is_approx(box.max, curr_box.max)) { - m_box.reset(); - - const Vec3f b_min = box.min.cast(); - const Vec3f b_max = box.max.cast(); - const Vec3f size = 0.2f * box.size().cast(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(48); - init_data.reserve_indices(48); - - // vertices - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y() + size.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z() + size.z())); - - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y() + size.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z() + size.z())); - - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y() - size.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z() + size.z())); - - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y() - size.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z() + size.z())); - - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y() + size.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z() - size.z())); - - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y() + size.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z() - size.z())); - - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y() - size.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z() - size.z())); - - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y() - size.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z() - size.z())); - - // indices - for (unsigned int i = 0; i < 48; ++i) { - init_data.add_index(i); - } - - m_box.init_from(std::move(init_data)); - } - - glsafe(::glEnable(GL_DEPTH_TEST)); - -#if ENABLE_GL_CORE_PROFILE - if (!OpenGLManager::get_gl_info().is_core_profile()) - glsafe(::glLineWidth(2.0f * m_scale_factor)); - - GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); -#else - glsafe(::glLineWidth(2.0f * m_scale_factor)); - GLShaderProgram* shader = wxGetApp().get_shader("flat"); -#endif // ENABLE_GL_CORE_PROFILE - if (shader == nullptr) - return; - -#if ENABLE_WORLD_COORDINATE -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPushMatrix()); - glsafe(::glMultMatrixd(trafo.data())); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -#endif // ENABLE_WORLD_COORDINATE - - shader->start_using(); - const Camera& camera = wxGetApp().plater()->get_camera(); -#if ENABLE_WORLD_COORDINATE - shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo); -#else - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); -#endif // ENABLE_WORLD_COORDINATE - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#if ENABLE_GL_CORE_PROFILE - const std::array& viewport = camera.get_viewport(); - shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); - shader->set_uniform("width", 1.5f); - shader->set_uniform("gap_size", 0.0f); -#endif // ENABLE_GL_CORE_PROFILE - m_box.set_color(to_rgba(color)); - m_box.render(); - shader->stop_using(); - -#if ENABLE_WORLD_COORDINATE -#if !ENABLE_LEGACY_OPENGL_REMOVAL - glsafe(::glPopMatrix()); -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL -#endif // ENABLE_WORLD_COORDINATE -#else - ::glBegin(GL_LINES); - - ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0) + size(0), b_min(1), b_min(2)); - ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0), b_min(1) + size(1), b_min(2)); - ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0), b_min(1), b_min(2) + size(2)); - - ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0) - size(0), b_min(1), b_min(2)); - ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0), b_min(1) + size(1), b_min(2)); - ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0), b_min(1), b_min(2) + size(2)); - - ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0) - size(0), b_max(1), b_min(2)); - ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0), b_max(1) - size(1), b_min(2)); - ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0), b_max(1), b_min(2) + size(2)); - - ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0) + size(0), b_max(1), b_min(2)); - ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0), b_max(1) - size(1), b_min(2)); - ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0), b_max(1), b_min(2) + size(2)); - - ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0) + size(0), b_min(1), b_max(2)); - ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0), b_min(1) + size(1), b_max(2)); - ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0), b_min(1), b_max(2) - size(2)); - - ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0) - size(0), b_min(1), b_max(2)); - ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0), b_min(1) + size(1), b_max(2)); - ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0), b_min(1), b_max(2) - size(2)); - - ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0) - size(0), b_max(1), b_max(2)); - ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0), b_max(1) - size(1), b_max(2)); - ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0), b_max(1), b_max(2) - size(2)); - - ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0) + size(0), b_max(1), b_max(2)); - ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0), b_max(1) - size(1), b_max(2)); - ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0), b_max(1), b_max(2) - size(2)); - - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -static ColorRGBA get_color(Axis axis) -{ - return AXES_COLOR[axis]; -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void Selection::render_sidebar_position_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) -#else -void Selection::render_sidebar_position_hints(const std::string& sidebar_field) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d& view_matrix = camera.get_view_matrix(); - shader.set_uniform("projection_matrix", camera.get_projection_matrix()); - - if (boost::ends_with(sidebar_field, "x")) { - const Transform3d model_matrix = matrix * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()); - shader.set_uniform("view_model_matrix", view_matrix * model_matrix); - const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader.set_uniform("view_normal_matrix", view_normal_matrix); - m_arrow.set_color(get_color(X)); - m_arrow.render(); - } - else if (boost::ends_with(sidebar_field, "y")) { - shader.set_uniform("view_model_matrix", view_matrix * matrix); - shader.set_uniform("view_normal_matrix", (Matrix3d)Matrix3d::Identity()); - m_arrow.set_color(get_color(Y)); - m_arrow.render(); - } - else if (boost::ends_with(sidebar_field, "z")) { - const Transform3d model_matrix = matrix * Geometry::rotation_transform(0.5 * PI * Vec3d::UnitX()); - shader.set_uniform("view_model_matrix", view_matrix * model_matrix); - const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader.set_uniform("view_normal_matrix", view_normal_matrix); - m_arrow.set_color(get_color(Z)); - m_arrow.render(); - } -#else - if (boost::ends_with(sidebar_field, "x")) { - glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); - m_arrow.set_color(-1, get_color(X)); - m_arrow.render(); - } - else if (boost::ends_with(sidebar_field, "y")) { - m_arrow.set_color(-1, get_color(Y)); - m_arrow.render(); - } - else if (boost::ends_with(sidebar_field, "z")) { - glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); - m_arrow.set_color(-1, get_color(Z)); - m_arrow.render(); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) -#else -void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - auto render_sidebar_rotation_hint = [this](GLShaderProgram& shader, const Transform3d& view_matrix, const Transform3d& model_matrix) { - shader.set_uniform("view_model_matrix", view_matrix * model_matrix); - Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader.set_uniform("view_normal_matrix", view_normal_matrix); - m_curved_arrow.render(); - const Transform3d matrix = model_matrix * Geometry::rotation_transform(PI * Vec3d::UnitZ()); - shader.set_uniform("view_model_matrix", view_matrix * matrix); - view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader.set_uniform("view_normal_matrix", view_normal_matrix); - m_curved_arrow.render(); - }; - - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d& view_matrix = camera.get_view_matrix(); - shader.set_uniform("projection_matrix", camera.get_projection_matrix()); - - if (boost::ends_with(sidebar_field, "x")) { - m_curved_arrow.set_color(get_color(X)); - render_sidebar_rotation_hint(shader, view_matrix, matrix * Geometry::rotation_transform(0.5 * PI * Vec3d::UnitY())); - } - else if (boost::ends_with(sidebar_field, "y")) { - m_curved_arrow.set_color(get_color(Y)); - render_sidebar_rotation_hint(shader, view_matrix, matrix * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitX())); - } - else if (boost::ends_with(sidebar_field, "z")) { - m_curved_arrow.set_color(get_color(Z)); - render_sidebar_rotation_hint(shader, view_matrix, matrix); - } -#else - auto render_sidebar_rotation_hint = [this]() { - m_curved_arrow.render(); - glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); - m_curved_arrow.render(); - }; - - if (boost::ends_with(sidebar_field, "x")) { - glsafe(::glRotated(90.0, 0.0, 1.0, 0.0)); - m_curved_arrow.set_color(-1, get_color(X)); - render_sidebar_rotation_hint(); - } - else if (boost::ends_with(sidebar_field, "y")) { - glsafe(::glRotated(-90.0, 1.0, 0.0, 0.0)); - m_curved_arrow.set_color(-1, get_color(Y)); - render_sidebar_rotation_hint(); - } - else if (boost::ends_with(sidebar_field, "z")) { - m_curved_arrow.set_color(-1, get_color(Z)); - render_sidebar_rotation_hint(); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void Selection::render_sidebar_scale_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) -#else -void Selection::render_sidebar_scale_hints(const std::string& sidebar_field) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ -#if ENABLE_WORLD_COORDINATE - const bool uniform_scale = wxGetApp().obj_manipul()->get_uniform_scaling(); -#else - const bool uniform_scale = requires_uniform_scale() || wxGetApp().obj_manipul()->get_uniform_scaling(); -#endif // ENABLE_WORLD_COORDINATE - -#if ENABLE_LEGACY_OPENGL_REMOVAL - auto render_sidebar_scale_hint = [this, uniform_scale](Axis axis, GLShaderProgram& shader, const Transform3d& view_matrix, const Transform3d& model_matrix) { - m_arrow.set_color(uniform_scale ? UNIFORM_SCALE_COLOR : get_color(axis)); - Transform3d matrix = model_matrix * Geometry::translation_transform(5.0 * Vec3d::UnitY()); - shader.set_uniform("view_model_matrix", view_matrix * matrix); - Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader.set_uniform("view_normal_matrix", view_normal_matrix); -#else - auto render_sidebar_scale_hint = [this, uniform_scale](Axis axis) { - m_arrow.set_color(-1, uniform_scale ? UNIFORM_SCALE_COLOR : get_color(axis)); - GLShaderProgram* shader = wxGetApp().get_current_shader(); - if (shader != nullptr) - shader->set_uniform("emission_factor", 0.0f); - - glsafe(::glTranslated(0.0, 5.0, 0.0)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - m_arrow.render(); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - matrix = model_matrix * Geometry::translation_transform(-5.0 * Vec3d::UnitY()) * Geometry::rotation_transform(PI * Vec3d::UnitZ()); - shader.set_uniform("view_model_matrix", view_matrix * matrix); - view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); - shader.set_uniform("view_normal_matrix", view_normal_matrix); -#else - glsafe(::glTranslated(0.0, -10.0, 0.0)); - glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - m_arrow.render(); - }; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d& view_matrix = camera.get_view_matrix(); - shader.set_uniform("projection_matrix", camera.get_projection_matrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - if (boost::ends_with(sidebar_field, "x") || uniform_scale) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - render_sidebar_scale_hint(X, shader, view_matrix, matrix * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ())); -#else - glsafe(::glPushMatrix()); - glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); - render_sidebar_scale_hint(X); - glsafe(::glPopMatrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - - if (boost::ends_with(sidebar_field, "y") || uniform_scale) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - render_sidebar_scale_hint(Y, shader, view_matrix, matrix); -#else - glsafe(::glPushMatrix()); - render_sidebar_scale_hint(Y); - glsafe(::glPopMatrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - - if (boost::ends_with(sidebar_field, "z") || uniform_scale) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - render_sidebar_scale_hint(Z, shader, view_matrix, matrix * Geometry::rotation_transform(0.5 * PI * Vec3d::UnitX())); -#else - glsafe(::glPushMatrix()); - glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); - render_sidebar_scale_hint(Z); - glsafe(::glPopMatrix()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void Selection::render_sidebar_layers_hints(const std::string& sidebar_field, GLShaderProgram& shader) -#else -void Selection::render_sidebar_layers_hints(const std::string& sidebar_field) -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -{ - static const float Margin = 10.0f; - - std::string field = sidebar_field; - - // extract max_z - std::string::size_type pos = field.rfind("_"); - if (pos == std::string::npos) - return; - - const float max_z = float(string_to_double_decimal_point(field.substr(pos + 1))); - - // extract min_z - field = field.substr(0, pos); - pos = field.rfind("_"); - if (pos == std::string::npos) - return; - - const float min_z = float(string_to_double_decimal_point(field.substr(pos + 1))); - - // extract type - field = field.substr(0, pos); - pos = field.rfind("_"); - if (pos == std::string::npos) - return; - - const int type = std::stoi(field.substr(pos + 1)); - - const BoundingBoxf3& box = get_bounding_box(); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - const float min_x = float(box.min.x()) - Margin; - const float max_x = float(box.max.x()) + Margin; - const float min_y = float(box.min.y()) - Margin; - const float max_y = float(box.max.y()) + Margin; -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - // view dependend order of rendering to keep correct transparency - const bool camera_on_top = wxGetApp().plater()->get_camera().is_looking_downward(); - const float z1 = camera_on_top ? min_z : max_z; - const float z2 = camera_on_top ? max_z : min_z; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Vec3f p1 = { float(box.min.x()) - Margin, float(box.min.y()) - Margin, z1 }; - const Vec3f p2 = { float(box.max.x()) + Margin, float(box.max.y()) + Margin, z2 }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glDisable(GL_CULL_FACE)); - glsafe(::glEnable(GL_BLEND)); - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_planes.models[0].is_initialized() || !is_approx(m_planes.check_points[0], p1)) { - m_planes.check_points[0] = p1; - m_planes.models[0].reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(4); - init_data.reserve_indices(6); - - // vertices - init_data.add_vertex(Vec3f(p1.x(), p1.y(), z1)); - init_data.add_vertex(Vec3f(p2.x(), p1.y(), z1)); - init_data.add_vertex(Vec3f(p2.x(), p2.y(), z1)); - init_data.add_vertex(Vec3f(p1.x(), p2.y(), z1)); - - // indices - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(2, 3, 0); - - m_planes.models[0].init_from(std::move(init_data)); - } - - if (!m_planes.models[1].is_initialized() || !is_approx(m_planes.check_points[1], p2)) { - m_planes.check_points[1] = p2; - m_planes.models[1].reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(4); - init_data.reserve_indices(6); - - // vertices - init_data.add_vertex(Vec3f(p1.x(), p1.y(), z2)); - init_data.add_vertex(Vec3f(p2.x(), p1.y(), z2)); - init_data.add_vertex(Vec3f(p2.x(), p2.y(), z2)); - init_data.add_vertex(Vec3f(p1.x(), p2.y(), z2)); - - // indices - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(2, 3, 0); - - m_planes.models[1].init_from(std::move(init_data)); - } - - const Camera& camera = wxGetApp().plater()->get_camera(); - shader.set_uniform("view_model_matrix", camera.get_view_matrix()); - shader.set_uniform("projection_matrix", camera.get_projection_matrix()); - - m_planes.models[0].set_color((camera_on_top && type == 1) || (!camera_on_top && type == 2) ? SOLID_PLANE_COLOR : TRANSPARENT_PLANE_COLOR); - m_planes.models[0].render(); - m_planes.models[1].set_color((camera_on_top && type == 2) || (!camera_on_top && type == 1) ? SOLID_PLANE_COLOR : TRANSPARENT_PLANE_COLOR); - m_planes.models[1].render(); -#else - ::glBegin(GL_QUADS); - ::glColor4fv((camera_on_top && type == 1) || (!camera_on_top && type == 2) ? SOLID_PLANE_COLOR.data() : TRANSPARENT_PLANE_COLOR.data()); - ::glVertex3f(min_x, min_y, z1); - ::glVertex3f(max_x, min_y, z1); - ::glVertex3f(max_x, max_y, z1); - ::glVertex3f(min_x, max_y, z1); - glsafe(::glEnd()); - - ::glBegin(GL_QUADS); - ::glColor4fv((camera_on_top && type == 2) || (!camera_on_top && type == 1) ? SOLID_PLANE_COLOR.data() : TRANSPARENT_PLANE_COLOR.data()); - ::glVertex3f(min_x, min_y, z2); - ::glVertex3f(max_x, min_y, z2); - ::glVertex3f(max_x, max_y, z2); - ::glVertex3f(min_x, max_y, z2); - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glEnable(GL_CULL_FACE)); - glsafe(::glDisable(GL_BLEND)); -} - -#ifndef NDEBUG -static bool is_rotation_xy_synchronized(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) -{ - const Eigen::AngleAxisd angle_axis(Geometry::rotation_xyz_diff(rot_xyz_from, rot_xyz_to)); - const Vec3d axis = angle_axis.axis(); - const double angle = angle_axis.angle(); - if (std::abs(angle) < 1e-8) - return true; - assert(std::abs(axis.x()) < 1e-8); - assert(std::abs(axis.y()) < 1e-8); - assert(std::abs(std::abs(axis.z()) - 1.) < 1e-8); - return std::abs(axis.x()) < 1e-8 && std::abs(axis.y()) < 1e-8 && std::abs(std::abs(axis.z()) - 1.) < 1e-8; -} - -static void verify_instances_rotation_synchronized(const Model &model, const GLVolumePtrs &volumes) -{ - for (int idx_object = 0; idx_object < int(model.objects.size()); ++idx_object) { - int idx_volume_first = -1; - for (int i = 0; i < (int)volumes.size(); ++i) { - if (volumes[i]->object_idx() == idx_object) { - idx_volume_first = i; - break; - } - } - assert(idx_volume_first != -1); // object without instances? - if (idx_volume_first == -1) - continue; - const Vec3d &rotation0 = volumes[idx_volume_first]->get_instance_rotation(); - for (int i = idx_volume_first + 1; i < (int)volumes.size(); ++i) - if (volumes[i]->object_idx() == idx_object) { - const Vec3d &rotation = volumes[i]->get_instance_rotation(); - assert(is_rotation_xy_synchronized(rotation, rotation0)); - } - } -} -#endif /* NDEBUG */ - -void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_type) -{ - std::set done; // prevent processing volumes twice - done.insert(m_list.begin(), m_list.end()); - - for (unsigned int i : m_list) { - if (done.size() == m_volumes->size()) - break; - - const GLVolume* volume_i = (*m_volumes)[i]; - if (volume_i->is_wipe_tower) - continue; - - const int object_idx = volume_i->object_idx(); - const int instance_idx = volume_i->instance_idx(); -#if ENABLE_WORLD_COORDINATE - const Geometry::Transformation& curr_inst_trafo_i = volume_i->get_instance_transformation(); - const Vec3d curr_inst_rotation_i = curr_inst_trafo_i.get_rotation(); - const Vec3d& curr_inst_scaling_factor_i = curr_inst_trafo_i.get_scaling_factor(); - const Vec3d& curr_inst_mirror_i = curr_inst_trafo_i.get_mirror(); - const Vec3d old_inst_rotation_i = m_cache.volumes_data[i].get_instance_transform().get_rotation(); -#else - const Vec3d& rotation = volume_i->get_instance_rotation(); - const Vec3d& scaling_factor = volume_i->get_instance_scaling_factor(); - const Vec3d& mirror = volume_i->get_instance_mirror(); -#endif // ENABLE_WORLD_COORDINATE - - // Process unselected instances. - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (done.size() == m_volumes->size()) - break; - - if (done.find(j) != done.end()) - continue; - - GLVolume* volume_j = (*m_volumes)[j]; - if (volume_j->object_idx() != object_idx || volume_j->instance_idx() == instance_idx) - continue; - -#if ENABLE_WORLD_COORDINATE - const Vec3d old_inst_rotation_j = m_cache.volumes_data[j].get_instance_transform().get_rotation(); - assert(is_rotation_xy_synchronized(old_inst_rotation_i, old_inst_rotation_j)); - const Geometry::Transformation& curr_inst_trafo_j = volume_j->get_instance_transformation(); - const Vec3d curr_inst_rotation_j = curr_inst_trafo_j.get_rotation(); - Vec3d new_inst_offset_j = curr_inst_trafo_j.get_offset(); - Vec3d new_inst_rotation_j = curr_inst_rotation_j; -#else - assert(is_rotation_xy_synchronized(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation())); -#endif // ENABLE_WORLD_COORDINATE - - switch (sync_rotation_type) { - case SyncRotationType::NONE: { - // z only rotation -> synch instance z - // The X,Y rotations should be synchronized from start to end of the rotation. -#if ENABLE_WORLD_COORDINATE - assert(is_rotation_xy_synchronized(curr_inst_rotation_i, curr_inst_rotation_j)); - if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) - new_inst_offset_j.z() = curr_inst_trafo_i.get_offset().z(); -#else - assert(is_rotation_xy_synchronized(rotation, volume_j->get_instance_rotation())); - if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) - volume_j->set_instance_offset(Z, volume_i->get_instance_offset().z()); -#endif // ENABLE_WORLD_COORDINATE - break; - } - case SyncRotationType::GENERAL: { - // generic rotation -> update instance z with the delta of the rotation. -#if ENABLE_WORLD_COORDINATE - const double z_diff = Geometry::rotation_diff_z(old_inst_rotation_i, old_inst_rotation_j); - new_inst_rotation_j = curr_inst_rotation_i + z_diff * Vec3d::UnitZ(); -#else - const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation()); - volume_j->set_instance_rotation({ rotation.x(), rotation.y(), rotation.z() + z_diff }); -#endif // ENABLE_WORLD_COORDINATE - break; - } -#if ENABLE_WORLD_COORDINATE - case SyncRotationType::FULL: { - // generic rotation -> update instance z with the delta of the rotation. - const Eigen::AngleAxisd angle_axis(Geometry::rotation_xyz_diff(curr_inst_rotation_i, old_inst_rotation_j)); - const Vec3d& axis = angle_axis.axis(); - const double z_diff = (std::abs(axis.x()) > EPSILON || std::abs(axis.y()) > EPSILON) ? - angle_axis.angle() * axis.z() : Geometry::rotation_diff_z(curr_inst_rotation_i, old_inst_rotation_j); - - new_inst_rotation_j = curr_inst_rotation_i + z_diff * Vec3d::UnitZ(); - break; - } -#endif // ENABLE_WORLD_COORDINATE - } - -#if ENABLE_WORLD_COORDINATE - volume_j->set_instance_transformation(Geometry::assemble_transform(new_inst_offset_j, new_inst_rotation_j, - curr_inst_scaling_factor_i, curr_inst_mirror_i)); -#else - volume_j->set_instance_scaling_factor(scaling_factor); - volume_j->set_instance_mirror(mirror); -#endif // ENABLE_WORLD_COORDINATE - - done.insert(j); - } - } - -#ifndef NDEBUG - verify_instances_rotation_synchronized(*m_model, *m_volumes); -#endif /* NDEBUG */ -} - -void Selection::synchronize_unselected_volumes() -{ - for (unsigned int i : m_list) { - const GLVolume* volume = (*m_volumes)[i]; - if (volume->is_wipe_tower) - continue; - - const int object_idx = volume->object_idx(); - const int volume_idx = volume->volume_idx(); -#if ENABLE_WORLD_COORDINATE - const Geometry::Transformation& trafo = volume->get_volume_transformation(); -#else - const Vec3d& offset = volume->get_volume_offset(); - const Vec3d& rotation = volume->get_volume_rotation(); - const Vec3d& scaling_factor = volume->get_volume_scaling_factor(); - const Vec3d& mirror = volume->get_volume_mirror(); -#endif // ENABLE_WORLD_COORDINATE - - // Process unselected volumes. - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (j == i) - continue; - - GLVolume* v = (*m_volumes)[j]; - if (v->object_idx() != object_idx || v->volume_idx() != volume_idx) - continue; - -#if ENABLE_WORLD_COORDINATE - v->set_volume_transformation(trafo); -#else - v->set_volume_offset(offset); - v->set_volume_rotation(rotation); - v->set_volume_scaling_factor(scaling_factor); - v->set_volume_mirror(mirror); -#endif // ENABLE_WORLD_COORDINATE - } - } -} - -void Selection::ensure_on_bed() -{ - typedef std::map, double> InstancesToZMap; - InstancesToZMap instances_min_z; - - for (size_t i = 0; i < m_volumes->size(); ++i) { - GLVolume* volume = (*m_volumes)[i]; - if (!volume->is_wipe_tower && !volume->is_modifier && - std::find(m_cache.sinking_volumes.begin(), m_cache.sinking_volumes.end(), i) == m_cache.sinking_volumes.end()) { - const double min_z = volume->transformed_convex_hull_bounding_box().min.z(); - std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::iterator it = instances_min_z.find(instance); - if (it == instances_min_z.end()) - it = instances_min_z.insert(InstancesToZMap::value_type(instance, DBL_MAX)).first; - - it->second = std::min(it->second, min_z); - } - } - - for (GLVolume* volume : *m_volumes) { - std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::iterator it = instances_min_z.find(instance); - if (it != instances_min_z.end()) - volume->set_instance_offset(Z, volume->get_instance_offset(Z) - it->second); - } -} - -void Selection::ensure_not_below_bed() -{ - typedef std::map, double> InstancesToZMap; - InstancesToZMap instances_max_z; - - for (size_t i = 0; i < m_volumes->size(); ++i) { - GLVolume* volume = (*m_volumes)[i]; - if (!volume->is_wipe_tower && !volume->is_modifier) { - const double max_z = volume->transformed_convex_hull_bounding_box().max.z(); - const std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::iterator it = instances_max_z.find(instance); - if (it == instances_max_z.end()) - it = instances_max_z.insert({ instance, -DBL_MAX }).first; - - it->second = std::max(it->second, max_z); - } - } - - if (is_any_volume()) { - for (unsigned int i : m_list) { - GLVolume& volume = *(*m_volumes)[i]; - const std::pair instance = std::make_pair(volume.object_idx(), volume.instance_idx()); - InstancesToZMap::const_iterator it = instances_max_z.find(instance); - const double z_shift = SINKING_MIN_Z_THRESHOLD - it->second; - if (it != instances_max_z.end() && z_shift > 0.0) - volume.set_volume_offset(Z, volume.get_volume_offset(Z) + z_shift); - } - } - else { - for (GLVolume* volume : *m_volumes) { - const std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::const_iterator it = instances_max_z.find(instance); - if (it != instances_max_z.end() && it->second < SINKING_MIN_Z_THRESHOLD) - volume->set_instance_offset(Z, volume->get_instance_offset(Z) + SINKING_MIN_Z_THRESHOLD - it->second); - } - } -} - -bool Selection::is_from_fully_selected_instance(unsigned int volume_idx) const -{ - struct SameInstance - { - int obj_idx; - int inst_idx; - GLVolumePtrs& volumes; - - SameInstance(int obj_idx, int inst_idx, GLVolumePtrs& volumes) : obj_idx(obj_idx), inst_idx(inst_idx), volumes(volumes) {} - bool operator () (unsigned int i) { return (volumes[i]->volume_idx() >= 0) && (volumes[i]->object_idx() == obj_idx) && (volumes[i]->instance_idx() == inst_idx); } - }; - - if ((unsigned int)m_volumes->size() <= volume_idx) - return false; - - GLVolume* volume = (*m_volumes)[volume_idx]; - int object_idx = volume->object_idx(); - if ((int)m_model->objects.size() <= object_idx) - return false; - - unsigned int count = (unsigned int)std::count_if(m_list.begin(), m_list.end(), SameInstance(object_idx, volume->instance_idx(), *m_volumes)); - return count == (unsigned int)m_model->objects[object_idx]->volumes.size(); -} - -void Selection::paste_volumes_from_clipboard() -{ -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ - - int dst_obj_idx = get_object_idx(); - if ((dst_obj_idx < 0) || ((int)m_model->objects.size() <= dst_obj_idx)) - return; - - ModelObject* dst_object = m_model->objects[dst_obj_idx]; - - int dst_inst_idx = get_instance_idx(); - if ((dst_inst_idx < 0) || ((int)dst_object->instances.size() <= dst_inst_idx)) - return; - - ModelObject* src_object = m_clipboard.get_object(0); - if (src_object != nullptr) - { - ModelInstance* dst_instance = dst_object->instances[dst_inst_idx]; - BoundingBoxf3 dst_instance_bb = dst_object->instance_bounding_box(dst_inst_idx); -#if ENABLE_WORLD_COORDINATE - Transform3d src_matrix = src_object->instances[0]->get_transformation().get_matrix_no_offset(); - Transform3d dst_matrix = dst_instance->get_transformation().get_matrix_no_offset(); -#else - Transform3d src_matrix = src_object->instances[0]->get_transformation().get_matrix(true); - Transform3d dst_matrix = dst_instance->get_transformation().get_matrix(true); -#endif // ENABLE_WORLD_COORDINATE - bool from_same_object = (src_object->input_file == dst_object->input_file) && src_matrix.isApprox(dst_matrix); - - // used to keep relative position of multivolume selections when pasting from another object - BoundingBoxf3 total_bb; - - ModelVolumePtrs volumes; - for (ModelVolume* src_volume : src_object->volumes) - { - ModelVolume* dst_volume = dst_object->add_volume(*src_volume); - dst_volume->set_new_unique_id(); - if (from_same_object) - { -// // if the volume comes from the same object, apply the offset in world system -// double offset = wxGetApp().plater()->canvas3D()->get_size_proportional_to_max_bed_size(0.05); -// dst_volume->translate(dst_matrix.inverse() * Vec3d(offset, offset, 0.0)); - } - else - { - // if the volume comes from another object, apply the offset as done when adding modifiers - // see ObjectList::load_generic_subobject() - total_bb.merge(dst_volume->mesh().bounding_box().transformed(src_volume->get_matrix())); - } - - volumes.push_back(dst_volume); -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ - } - - // keeps relative position of multivolume selections - if (!from_same_object) - { - for (ModelVolume* v : volumes) - { - v->set_offset((v->get_offset() - total_bb.center()) + dst_matrix.inverse() * (Vec3d(dst_instance_bb.max(0), dst_instance_bb.min(1), dst_instance_bb.min(2)) + 0.5 * total_bb.size() - dst_instance->get_transformation().get_offset())); - } - } - - wxGetApp().obj_list()->paste_volumes_into_list(dst_obj_idx, volumes); - } - -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ -} - -void Selection::paste_objects_from_clipboard() -{ -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ - - std::vector object_idxs; - const ModelObjectPtrs& src_objects = m_clipboard.get_objects(); - for (const ModelObject* src_object : src_objects) - { - ModelObject* dst_object = m_model->add_object(*src_object); - double offset = wxGetApp().plater()->canvas3D()->get_size_proportional_to_max_bed_size(0.05); - Vec3d displacement(offset, offset, 0.0); - for (ModelInstance* inst : dst_object->instances) - { - inst->set_offset(inst->get_offset() + displacement); - } - - object_idxs.push_back(m_model->objects.size() - 1); -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ - } - - wxGetApp().obj_list()->paste_objects_into_list(object_idxs); - -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ -} - -#if ENABLE_WORLD_COORDINATE -void Selection::transform_volume_relative(GLVolume& volume, const VolumeCache& volume_data, TransformationType transformation_type, - const Transform3d& transform) -{ - const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); - const Geometry::Transformation& volume_trafo = volume_data.get_volume_transform(); - if (transformation_type.world()) { - const Transform3d inst_matrix_no_offset = inst_trafo.get_matrix_no_offset(); - const Transform3d new_volume_matrix = inst_matrix_no_offset.inverse() * transform * inst_matrix_no_offset; - volume.set_volume_transformation(volume_trafo.get_offset_matrix() * new_volume_matrix * volume_trafo.get_matrix_no_offset()); - } - else if (transformation_type.instance()) - volume.set_volume_transformation(volume_trafo.get_offset_matrix() * transform * volume_trafo.get_matrix_no_offset()); - else if (transformation_type.local()) { - const Geometry::Transformation trafo(transform); - volume.set_volume_transformation(trafo.get_offset_matrix() * volume_trafo.get_matrix() * trafo.get_matrix_no_offset()); - } - else - assert(false); -} -#endif // ENABLE_WORLD_COORDINATE - -} // namespace GUI -} // namespace Slic3r +#include "libslic3r/libslic3r.h" +#include "Selection.hpp" + +#include "3DScene.hpp" +#include "GLCanvas3D.hpp" +#include "GUI_App.hpp" +#include "GUI.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "GUI_ObjectList.hpp" +#include "Camera.hpp" +#include "Plater.hpp" +#if ENABLE_WORLD_COORDINATE +#include "MsgDialog.hpp" +#endif // ENABLE_WORLD_COORDINATE + +#include "Gizmos/GLGizmoBase.hpp" + +#include "slic3r/Utils/UndoRedo.hpp" + +#include "libslic3r/LocalesUtils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/BuildVolume.hpp" + +#include + +#include +#include + +static const Slic3r::ColorRGBA UNIFORM_SCALE_COLOR = Slic3r::ColorRGBA::ORANGE(); +static const Slic3r::ColorRGBA SOLID_PLANE_COLOR = Slic3r::ColorRGBA::ORANGE(); +static const Slic3r::ColorRGBA TRANSPARENT_PLANE_COLOR = { 0.8f, 0.8f, 0.8f, 0.5f }; + +namespace Slic3r { +namespace GUI { + +Selection::VolumeCache::TransformCache::TransformCache(const Geometry::Transformation& transform) + : position(transform.get_offset()) + , rotation(transform.get_rotation()) + , scaling_factor(transform.get_scaling_factor()) + , mirror(transform.get_mirror()) + , full_matrix(transform.get_matrix()) +#if ENABLE_WORLD_COORDINATE + , transform(transform) + , rotation_matrix(transform.get_rotation_matrix()) + , scale_matrix(transform.get_scaling_factor_matrix()) + , mirror_matrix(transform.get_mirror_matrix()) +#endif // ENABLE_WORLD_COORDINATE +{ +#if !ENABLE_WORLD_COORDINATE + rotation_matrix = Geometry::assemble_transform(Vec3d::Zero(), rotation); + scale_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scaling_factor); + mirror_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), Vec3d::Ones(), mirror); +#endif // !ENABLE_WORLD_COORDINATE +} + +Selection::VolumeCache::VolumeCache(const Geometry::Transformation& volume_transform, const Geometry::Transformation& instance_transform) + : m_volume(volume_transform) + , m_instance(instance_transform) +{ +} + +bool Selection::Clipboard::is_sla_compliant() const +{ + if (m_mode == Selection::Volume) + return false; + + for (const ModelObject* o : m_model->objects) { + if (o->is_multiparts()) + return false; + + for (const ModelVolume* v : o->volumes) { + if (v->is_modifier()) + return false; + } + } + + return true; +} + +Selection::Clipboard::Clipboard() +{ + m_model.reset(new Model); +} + +void Selection::Clipboard::reset() +{ + m_model->clear_objects(); +} + +bool Selection::Clipboard::is_empty() const +{ + return m_model->objects.empty(); +} + +ModelObject* Selection::Clipboard::add_object() +{ + return m_model->add_object(); +} + +ModelObject* Selection::Clipboard::get_object(unsigned int id) +{ + return (id < (unsigned int)m_model->objects.size()) ? m_model->objects[id] : nullptr; +} + +const ModelObjectPtrs& Selection::Clipboard::get_objects() const +{ + return m_model->objects; +} + +Selection::Selection() + : m_volumes(nullptr) + , m_model(nullptr) + , m_enabled(false) + , m_mode(Instance) + , m_type(Empty) + , m_valid(false) + , m_scale_factor(1.0f) +{ + this->set_bounding_boxes_dirty(); +#if ENABLE_WORLD_COORDINATE + m_axes.set_stem_radius(0.15f); + m_axes.set_stem_length(3.0f); + m_axes.set_tip_radius(0.45f); + m_axes.set_tip_length(1.5f); +#endif // ENABLE_WORLD_COORDINATE +} + + +void Selection::set_volumes(GLVolumePtrs* volumes) +{ + m_volumes = volumes; + update_valid(); +} + +// Init shall be called from the OpenGL render function, so that the OpenGL context is initialized! +bool Selection::init() +{ + m_arrow.init_from(straight_arrow(10.0f, 5.0f, 5.0f, 10.0f, 1.0f)); + m_curved_arrow.init_from(circular_arrow(16, 10.0f, 5.0f, 10.0f, 5.0f, 1.0f)); +#if ENABLE_RENDER_SELECTION_CENTER + m_vbo_sphere.init_from(its_make_sphere(0.75, PI / 12.0)); +#endif // ENABLE_RENDER_SELECTION_CENTER + + return true; +} + +void Selection::set_model(Model* model) +{ + m_model = model; + update_valid(); +} + +void Selection::add(unsigned int volume_idx, bool as_single_selection, bool check_for_already_contained) +{ + if (!m_valid || (unsigned int)m_volumes->size() <= volume_idx) + return; + + const GLVolume* volume = (*m_volumes)[volume_idx]; + // wipe tower is already selected + if (is_wipe_tower() && volume->is_wipe_tower) + return; + + bool keep_instance_mode = (m_mode == Instance) && !as_single_selection; + bool already_contained = check_for_already_contained && contains_volume(volume_idx); + + // resets the current list if needed + bool needs_reset = as_single_selection && !already_contained; + needs_reset |= volume->is_wipe_tower; + needs_reset |= is_wipe_tower() && !volume->is_wipe_tower; + needs_reset |= as_single_selection && !is_any_modifier() && volume->is_modifier; + needs_reset |= is_any_modifier() && !volume->is_modifier; + + if (!already_contained || needs_reset) { + wxGetApp().plater()->take_snapshot(_L("Selection-Add"), UndoRedo::SnapshotType::Selection); + + if (needs_reset) + clear(); + + if (!keep_instance_mode) + m_mode = volume->is_modifier ? Volume : Instance; + } + else + // keep current mode + return; + + switch (m_mode) + { + case Volume: + { + if (volume->volume_idx() >= 0 && (is_empty() || volume->instance_idx() == get_instance_idx())) + do_add_volume(volume_idx); + + break; + } + case Instance: + { + Plater::SuppressSnapshots suppress(wxGetApp().plater()); + add_instance(volume->object_idx(), volume->instance_idx(), as_single_selection); + break; + } + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove(unsigned int volume_idx) +{ + if (!m_valid || (unsigned int)m_volumes->size() <= volume_idx) + return; + + if (!contains_volume(volume_idx)) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Remove"), UndoRedo::SnapshotType::Selection); + + GLVolume* volume = (*m_volumes)[volume_idx]; + + switch (m_mode) + { + case Volume: + { + do_remove_volume(volume_idx); + break; + } + case Instance: + { + do_remove_instance(volume->object_idx(), volume->instance_idx()); + break; + } + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_object(unsigned int object_idx, bool as_single_selection) +{ + if (!m_valid) + return; + + std::vector volume_idxs = get_volume_idxs_from_object(object_idx); + if ((!as_single_selection && contains_all_volumes(volume_idxs)) || + (as_single_selection && matches(volume_idxs))) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Add Object"), UndoRedo::SnapshotType::Selection); + + // resets the current list if needed + if (as_single_selection) + clear(); + + m_mode = Instance; + + do_add_volumes(volume_idxs); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_object(unsigned int object_idx) +{ + if (!m_valid) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Remove Object"), UndoRedo::SnapshotType::Selection); + + do_remove_object(object_idx); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_instance(unsigned int object_idx, unsigned int instance_idx, bool as_single_selection) +{ + if (!m_valid) + return; + + const std::vector volume_idxs = get_volume_idxs_from_instance(object_idx, instance_idx); + if ((!as_single_selection && contains_all_volumes(volume_idxs)) || + (as_single_selection && matches(volume_idxs))) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Add Instance"), UndoRedo::SnapshotType::Selection); + + // resets the current list if needed + if (as_single_selection) + clear(); + + m_mode = Instance; + + do_add_volumes(volume_idxs); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_instance(unsigned int object_idx, unsigned int instance_idx) +{ + if (!m_valid) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Remove Instance"), UndoRedo::SnapshotType::Selection); + + do_remove_instance(object_idx, instance_idx); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_volume(unsigned int object_idx, unsigned int volume_idx, int instance_idx, bool as_single_selection) +{ + if (!m_valid) + return; + + std::vector volume_idxs = get_volume_idxs_from_volume(object_idx, instance_idx, volume_idx); + if ((!as_single_selection && contains_all_volumes(volume_idxs)) || + (as_single_selection && matches(volume_idxs))) + return; + + // resets the current list if needed + if (as_single_selection) + clear(); + + m_mode = Volume; + + do_add_volumes(volume_idxs); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_volume(unsigned int object_idx, unsigned int volume_idx) +{ + if (!m_valid) + return; + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx && v->volume_idx() == (int)volume_idx) + do_remove_volume(i); + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_volumes(EMode mode, const std::vector& volume_idxs, bool as_single_selection) +{ + if (!m_valid) + return; + + if ((!as_single_selection && contains_all_volumes(volume_idxs)) || + (as_single_selection && matches(volume_idxs))) + return; + + // resets the current list if needed + if (as_single_selection) + clear(); + + m_mode = mode; + for (unsigned int i : volume_idxs) { + if (i < (unsigned int)m_volumes->size()) + do_add_volume(i); + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_volumes(EMode mode, const std::vector& volume_idxs) +{ + if (!m_valid) + return; + + m_mode = mode; + for (unsigned int i : volume_idxs) { + if (i < (unsigned int)m_volumes->size()) + do_remove_volume(i); + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_all() +{ + if (!m_valid) + return; + + unsigned int count = 0; + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + if (!(*m_volumes)[i]->is_wipe_tower) + ++count; + } + + if ((unsigned int)m_list.size() == count) + return; + + wxGetApp().plater()->take_snapshot(_(L("Selection-Add All")), UndoRedo::SnapshotType::Selection); + + m_mode = Instance; + clear(); + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + if (!(*m_volumes)[i]->is_wipe_tower) + do_add_volume(i); + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_all() +{ + if (!m_valid) + return; + + if (is_empty()) + return; + +// Not taking the snapshot with non-empty Redo stack will likely be more confusing than losing the Redo stack. +// Let's wait for user feedback. +// if (!wxGetApp().plater()->can_redo()) + wxGetApp().plater()->take_snapshot(_L("Selection-Remove All"), UndoRedo::SnapshotType::Selection); + + m_mode = Instance; + clear(); +} + +void Selection::set_deserialized(EMode mode, const std::vector> &volumes_and_instances) +{ + if (! m_valid) + return; + + m_mode = mode; + for (unsigned int i : m_list) + (*m_volumes)[i]->selected = false; + m_list.clear(); + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++ i) + if (std::binary_search(volumes_and_instances.begin(), volumes_and_instances.end(), (*m_volumes)[i]->geometry_id)) + do_add_volume(i); + update_type(); + set_bounding_boxes_dirty(); +} + +void Selection::clear() +{ + if (!m_valid) + return; + + if (m_list.empty()) + return; + + // ensure that the volumes get the proper color before next call to render (expecially needed for transparent volumes) + for (unsigned int i : m_list) { + GLVolume& volume = *(*m_volumes)[i]; + volume.selected = false; + volume.set_render_color(volume.color.is_transparent()); + } + + m_list.clear(); + + update_type(); + set_bounding_boxes_dirty(); + + // this happens while the application is closing + if (wxGetApp().obj_manipul() == nullptr) + return; + + // resets the cache in the sidebar + wxGetApp().obj_manipul()->reset_cache(); + + // #et_FIXME fake KillFocus from sidebar + wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event("", false); +} + +// Update the selection based on the new instance IDs. +void Selection::instances_changed(const std::vector &instance_ids_selected) +{ + assert(m_valid); + assert(m_mode == Instance); + m_list.clear(); + for (unsigned int volume_idx = 0; volume_idx < (unsigned int)m_volumes->size(); ++ volume_idx) { + const GLVolume *volume = (*m_volumes)[volume_idx]; + auto it = std::lower_bound(instance_ids_selected.begin(), instance_ids_selected.end(), volume->geometry_id.second); + if (it != instance_ids_selected.end() && *it == volume->geometry_id.second) + this->do_add_volume(volume_idx); + } + update_type(); + this->set_bounding_boxes_dirty(); +} + +// Update the selection based on the map from old indices to new indices after m_volumes changed. +// If the current selection is by instance, this call may select newly added volumes, if they belong to already selected instances. +void Selection::volumes_changed(const std::vector &map_volume_old_to_new) +{ + assert(m_valid); + assert(m_mode == Volume); + IndicesList list_new; + for (unsigned int idx : m_list) + if (map_volume_old_to_new[idx] != size_t(-1)) { + unsigned int new_idx = (unsigned int)map_volume_old_to_new[idx]; + (*m_volumes)[new_idx]->selected = true; + list_new.insert(new_idx); + } + m_list = std::move(list_new); + update_type(); + this->set_bounding_boxes_dirty(); +} + +bool Selection::is_any_connector() const +{ + const int obj_idx = get_object_idx(); + + if ((is_any_volume() || is_any_modifier() || is_mixed()) && // some solid_part AND/OR modifier is selected + obj_idx >= 0 && m_model->objects[obj_idx]->is_cut()) { + const ModelVolumePtrs& obj_volumes = m_model->objects[obj_idx]->volumes; + for (size_t vol_idx = 0; vol_idx < obj_volumes.size(); vol_idx++) + if (obj_volumes[vol_idx]->is_cut_connector()) + for (const GLVolume* v : *m_volumes) + if (v->object_idx() == obj_idx && v->volume_idx() == (int)vol_idx && v->selected) + return true; + } + return false; +} + +bool Selection::is_any_cut_volume() const +{ + const int obj_idx = get_object_idx(); + return is_any_volume() && obj_idx >= 0 && m_model->objects[obj_idx]->is_cut(); +} + +bool Selection::is_single_full_instance() const +{ + if (m_type == SingleFullInstance) + return true; + + if (m_type == SingleFullObject) + return get_instance_idx() != -1; + + if (m_list.empty() || m_volumes->empty()) + return false; + + int object_idx = m_valid ? get_object_idx() : -1; + if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) + return false; + + int instance_idx = (*m_volumes)[*m_list.begin()]->instance_idx(); + + std::set volumes_idxs; + for (unsigned int i : m_list) { + const GLVolume* v = (*m_volumes)[i]; + if (object_idx != v->object_idx() || instance_idx != v->instance_idx()) + return false; + + int volume_idx = v->volume_idx(); + if (volume_idx >= 0) + volumes_idxs.insert(volume_idx); + } + + return m_model->objects[object_idx]->volumes.size() == volumes_idxs.size(); +} + +bool Selection::is_from_single_object() const +{ + const int idx = get_object_idx(); + return 0 <= idx && idx < int(m_model->objects.size()); +} + +bool Selection::is_sla_compliant() const +{ + if (m_mode == Volume) + return false; + + for (unsigned int i : m_list) { + if ((*m_volumes)[i]->is_modifier) + return false; + } + + return true; +} + +bool Selection::is_single_text() const +{ + if (!is_single_volume_or_modifier()) + return false; + + const GLVolume* gl_volume = (*m_volumes)[*m_list.begin()]; + const ModelVolume* model_volume = m_model->objects[gl_volume->object_idx()]->volumes[gl_volume->volume_idx()]; + + return model_volume && model_volume->text_configuration.has_value(); +} + +bool Selection::contains_all_volumes(const std::vector& volume_idxs) const +{ + for (unsigned int i : volume_idxs) { + if (m_list.find(i) == m_list.end()) + return false; + } + + return true; +} + +bool Selection::contains_any_volume(const std::vector& volume_idxs) const +{ + for (unsigned int i : volume_idxs) { + if (m_list.find(i) != m_list.end()) + return true; + } + + return false; +} + +bool Selection::matches(const std::vector& volume_idxs) const +{ + unsigned int count = 0; + + for (unsigned int i : volume_idxs) { + if (m_list.find(i) != m_list.end()) + ++count; + else + return false; + } + + return count == (unsigned int)m_list.size(); +} + +#if !ENABLE_WORLD_COORDINATE +bool Selection::requires_uniform_scale() const +{ + if (is_single_full_instance() || is_single_modifier() || is_single_volume()) + return false; + + return true; +} +#endif // !ENABLE_WORLD_COORDINATE + +int Selection::get_object_idx() const +{ + return (m_cache.content.size() == 1) ? m_cache.content.begin()->first : -1; +} + +int Selection::get_instance_idx() const +{ + if (m_cache.content.size() == 1) { + const InstanceIdxsList& idxs = m_cache.content.begin()->second; + if (idxs.size() == 1) + return *idxs.begin(); + } + + return -1; +} + +const Selection::InstanceIdxsList& Selection::get_instance_idxs() const +{ + assert(m_cache.content.size() == 1); + return m_cache.content.begin()->second; +} + +const GLVolume* Selection::get_volume(unsigned int volume_idx) const +{ + return (m_valid && (volume_idx < (unsigned int)m_volumes->size())) ? (*m_volumes)[volume_idx] : nullptr; +} + +GLVolume* Selection::get_volume(unsigned int volume_idx) +{ + return (m_valid && (volume_idx < (unsigned int)m_volumes->size())) ? (*m_volumes)[volume_idx] : nullptr; +} + +const BoundingBoxf3& Selection::get_bounding_box() const +{ + if (!m_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + (*bbox)->merge((*m_volumes)[i]->transformed_convex_hull_bounding_box()); + } + } + } + return *m_bounding_box; +} + +const BoundingBoxf3& Selection::get_unscaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_unscaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_unscaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + if (volume.is_modifier) + continue; +#if ENABLE_WORLD_COORDINATE + Transform3d trafo = volume.get_instance_transformation().get_matrix_no_scaling_factor() * volume.get_volume_transformation().get_matrix(); +#else + Transform3d trafo = volume.get_instance_transformation().get_matrix(false, false, true, false) * volume.get_volume_transformation().get_matrix(); +#endif // ENABLE_WORLD_COORDINATE + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_unscaled_instance_bounding_box; +} + +const BoundingBoxf3& Selection::get_scaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_scaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_scaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + if (volume.is_modifier) + continue; + Transform3d trafo = volume.get_instance_transformation().get_matrix() * volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_scaled_instance_bounding_box; +} + +#if ENABLE_WORLD_COORDINATE +const BoundingBoxf3& Selection::get_full_unscaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_full_unscaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_full_unscaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + Transform3d trafo = volume.get_instance_transformation().get_matrix_no_scaling_factor() * volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_full_unscaled_instance_bounding_box; +} + +const BoundingBoxf3& Selection::get_full_scaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_full_scaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_full_scaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + Transform3d trafo = volume.get_instance_transformation().get_matrix() * volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_full_scaled_instance_bounding_box; +} + +const BoundingBoxf3& Selection::get_full_unscaled_instance_local_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_full_unscaled_instance_local_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_full_unscaled_instance_local_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + Transform3d trafo = volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_full_unscaled_instance_local_bounding_box; +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::setup_cache() +{ + if (!m_valid) + return; + + set_caches(); +} + +#if ENABLE_WORLD_COORDINATE +void Selection::translate(const Vec3d& displacement, TransformationType transformation_type) +{ + if (!m_valid) + return; + + // Emboss use translate in local coordinate + assert(transformation_type.relative() || + transformation_type.local()); + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + if (m_mode == Instance && !is_wipe_tower()) { + assert(is_from_fully_selected_instance(i)); + if (transformation_type.world()) + v.set_instance_transformation(Geometry::translation_transform(displacement) * volume_data.get_instance_full_matrix()); + else if (transformation_type.local()) { + const Vec3d world_displacement = volume_data.get_instance_rotation_matrix() * displacement; + v.set_instance_transformation(Geometry::translation_transform(world_displacement) * volume_data.get_instance_full_matrix()); + } + else + assert(false); + } + else { + const Vec3d offset = transformation_type.local() ? + (Vec3d)(volume_data.get_volume_transform().get_rotation_matrix() * displacement) : displacement; + transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(offset)); + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + ensure_not_below_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#else +void Selection::translate(const Vec3d& displacement, bool local) +{ + if (!m_valid) + return; + + EMode translation_type = m_mode; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + if (m_mode == Volume || v.is_wipe_tower) { + if (local) + v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + displacement); + else { + const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; + v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); + } + } + else if (m_mode == Instance) { + if (is_from_fully_selected_instance(i)) + v.set_instance_offset(m_cache.volumes_data[i].get_instance_position() + displacement); + else { + const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; + v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); + translation_type = Volume; + } + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (translation_type == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (translation_type == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + ensure_not_below_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#endif // ENABLE_WORLD_COORDINATE + +// Rotate an object around one of the axes. Only one rotation component is expected to be changing. +#if ENABLE_WORLD_COORDINATE +void Selection::rotate(const Vec3d& rotation, TransformationType transformation_type) +{ + if (!m_valid) + return; + + assert(transformation_type.relative() || (transformation_type.absolute() && transformation_type.local())); + + const Transform3d rotation_matrix = Geometry::rotation_transform(rotation); + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + if (m_mode == Instance && !is_wipe_tower()) { + assert(is_from_fully_selected_instance(i)); + Transform3d new_rotation_matrix = Transform3d::Identity(); + if (transformation_type.absolute()) + new_rotation_matrix = rotation_matrix; + else { + if (transformation_type.world()) + new_rotation_matrix = rotation_matrix * inst_trafo.get_rotation_matrix(); + else if (transformation_type.local()) + new_rotation_matrix = inst_trafo.get_rotation_matrix() * rotation_matrix; + else + assert(false); + } + + const Vec3d new_offset = transformation_type.independent() ? inst_trafo.get_offset() : + m_cache.dragging_center + new_rotation_matrix * inst_trafo.get_rotation_matrix().inverse() * + (inst_trafo.get_offset() - m_cache.dragging_center); + v.set_instance_transformation(Geometry::assemble_transform(Geometry::translation_transform(new_offset), new_rotation_matrix, + inst_trafo.get_scaling_factor_matrix(), inst_trafo.get_mirror_matrix())); + } + else { + if (transformation_type.absolute()) { + const Geometry::Transformation& volume_trafo = volume_data.get_volume_transform(); + v.set_volume_transformation(Geometry::assemble_transform(volume_trafo.get_offset_matrix(), Geometry::rotation_transform(rotation), + volume_trafo.get_scaling_factor_matrix(), volume_trafo.get_mirror_matrix())); + } + else + transform_volume_relative(v, volume_data, transformation_type, Geometry::rotation_transform(rotation)); + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) { + int rot_axis_max = 0; + rotation.cwiseAbs().maxCoeff(&rot_axis_max); + SyncRotationType synch; + if (transformation_type.world() && rot_axis_max == 2) + synch = SyncRotationType::NONE; + else if (transformation_type.local()) + synch = SyncRotationType::FULL; + else + synch = SyncRotationType::GENERAL; + synchronize_unselected_instances(synch); + } + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#else +void Selection::rotate(const Vec3d& rotation, TransformationType transformation_type) +{ + if (!m_valid) + return; + + // Only relative rotation values are allowed in the world coordinate system. + assert(!transformation_type.world() || transformation_type.relative()); + + if (!is_wipe_tower()) { + int rot_axis_max = 0; + if (rotation.isApprox(Vec3d::Zero())) { + for (unsigned int i : m_list) { + GLVolume &v = *(*m_volumes)[i]; + if (m_mode == Instance) { + v.set_instance_rotation(m_cache.volumes_data[i].get_instance_rotation()); + v.set_instance_offset(m_cache.volumes_data[i].get_instance_position()); + } + else if (m_mode == Volume) { + v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation()); + v.set_volume_offset(m_cache.volumes_data[i].get_volume_position()); + } + } + } + else { // this is not the wipe tower + //FIXME this does not work for absolute rotations (transformation_type.absolute() is true) + rotation.cwiseAbs().maxCoeff(&rot_axis_max); + +// if ( single instance or single volume ) + // Rotate around center , if only a single object or volume +// transformation_type.set_independent(); + + // For generic rotation, we want to rotate the first volume in selection, and then to synchronize the other volumes with it. + std::vector object_instance_first(m_model->objects.size(), -1); + auto rotate_instance = [this, &rotation, &object_instance_first, rot_axis_max, transformation_type](GLVolume &volume, int i) { + const int first_volume_idx = object_instance_first[volume.object_idx()]; + if (rot_axis_max != 2 && first_volume_idx != -1) { + // Generic rotation, but no rotation around the Z axis. + // Always do a local rotation (do not consider the selection to be a rigid body). + assert(is_approx(rotation.z(), 0.0)); + const GLVolume &first_volume = *(*m_volumes)[first_volume_idx]; + const Vec3d &rotation = first_volume.get_instance_rotation(); + const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[first_volume_idx].get_instance_rotation(), m_cache.volumes_data[i].get_instance_rotation()); + volume.set_instance_rotation(Vec3d(rotation.x(), rotation.y(), rotation.z() + z_diff)); + } + else { + // extracts rotations from the composed transformation + const Vec3d new_rotation = transformation_type.world() ? + Geometry::extract_rotation(Geometry::assemble_transform(Vec3d::Zero(), rotation) * m_cache.volumes_data[i].get_instance_rotation_matrix()) : + transformation_type.absolute() ? rotation : rotation + m_cache.volumes_data[i].get_instance_rotation(); + if (rot_axis_max == 2 && transformation_type.joint()) { + // Only allow rotation of multiple instances as a single rigid body when rotating around the Z axis. + const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), new_rotation); + volume.set_instance_offset(m_cache.dragging_center + Eigen::AngleAxisd(z_diff, Vec3d::UnitZ()) * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); + } + volume.set_instance_rotation(new_rotation); + object_instance_first[volume.object_idx()] = i; + } + }; + + for (unsigned int i : m_list) { + GLVolume &v = *(*m_volumes)[i]; + if (is_single_full_instance()) + rotate_instance(v, i); + else if (is_single_volume() || is_single_modifier()) { + if (transformation_type.independent()) + v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation() + rotation); + else { + const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); + const Vec3d new_rotation = Geometry::extract_rotation(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); + v.set_volume_rotation(new_rotation); + } + } + else { + if (m_mode == Instance) + rotate_instance(v, i); + else if (m_mode == Volume) { + // extracts rotations from the composed transformation + const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); + const Vec3d new_rotation = Geometry::extract_rotation(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); + if (transformation_type.joint()) { + const Vec3d local_pivot = m_cache.volumes_data[i].get_instance_full_matrix().inverse() * m_cache.dragging_center; + const Vec3d offset = m * (m_cache.volumes_data[i].get_volume_position() - local_pivot); + v.set_volume_offset(local_pivot + offset); + } + v.set_volume_rotation(new_rotation); + } + } + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances((rot_axis_max == 2) ? SyncRotationType::NONE : SyncRotationType::GENERAL); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + } + else { // it's the wipe tower that's selected and being rotated + GLVolume& volume = *((*m_volumes)[*m_list.begin()]); // the wipe tower is always alone in the selection + + // make sure the wipe tower rotates around its center, not origin + // we can assume that only Z rotation changes + const Vec3d center_local = volume.transformed_bounding_box().center() - volume.get_volume_offset(); + const Vec3d center_local_new = Eigen::AngleAxisd(rotation.z()-volume.get_volume_rotation().z(), Vec3d::UnitZ()) * center_local; + volume.set_volume_rotation(rotation); + volume.set_volume_offset(volume.get_volume_offset() + center_local - center_local_new); + } + + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::flattening_rotate(const Vec3d& normal) +{ + // We get the normal in untransformed coordinates. We must transform it using the instance matrix, find out + // how to rotate the instance so it faces downwards and do the rotation. All that for all selected instances. + // The function assumes that is_from_single_object() holds. + assert(Slic3r::is_approx(normal.norm(), 1.)); + + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + // Normal transformed from the object coordinate space to the world coordinate space. +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& old_inst_trafo = v.get_instance_transformation(); + const Vec3d tnormal = old_inst_trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal; + // Additional rotation to align tnormal with the down vector in the world coordinate space. + const Transform3d rotation_matrix = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ())); + v.set_instance_transformation(old_inst_trafo.get_offset_matrix() * rotation_matrix * old_inst_trafo.get_matrix_no_offset()); +#else + const auto& voldata = m_cache.volumes_data[i]; + Vec3d tnormal = (Geometry::assemble_transform( + Vec3d::Zero(), voldata.get_instance_rotation(), + voldata.get_instance_scaling_factor().cwiseInverse(), voldata.get_instance_mirror()) * normal).normalized(); + // Additional rotation to align tnormal with the down vector in the world coordinate space. + auto extra_rotation = Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()); + v.set_instance_rotation(Geometry::extract_rotation(extra_rotation.toRotationMatrix() * m_cache.volumes_data[i].get_instance_rotation_matrix())); +#endif // ENABLE_WORLD_COORDINATE + } + +#if !DISABLE_INSTANCES_SYNCH + // Apply the same transformation also to other instances, + // but respect their possibly diffrent z-rotation. + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::GENERAL); +#endif // !DISABLE_INSTANCES_SYNCH + + this->set_bounding_boxes_dirty(); +} + +#if ENABLE_WORLD_COORDINATE +void Selection::scale(const Vec3d& scale, TransformationType transformation_type) +{ + scale_and_translate(scale, Vec3d::Zero(), transformation_type); +} +#else +void Selection::scale(const Vec3d& scale, TransformationType transformation_type) +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume &v = *(*m_volumes)[i]; + if (is_single_full_instance()) { + if (transformation_type.relative()) { + const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scale); + const Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_instance_scale_matrix()).matrix().block(0, 0, 3, 3); + // extracts scaling factors from the composed transformation + const Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); + if (transformation_type.joint()) + v.set_instance_offset(m_cache.dragging_center + m * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); + + v.set_instance_scaling_factor(new_scale); + } + else { + if (transformation_type.world() && (std::abs(scale.x() - scale.y()) > EPSILON || std::abs(scale.x() - scale.z()) > EPSILON)) { + // Non-uniform scaling. Transform the scaling factors into the local coordinate system. + // This is only possible, if the instance rotation is mulitples of ninety degrees. + assert(Geometry::is_rotation_ninety_degrees(v.get_instance_rotation())); + v.set_instance_scaling_factor((v.get_instance_transformation().get_matrix(true, false, true, true).matrix().block<3, 3>(0, 0).transpose() * scale).cwiseAbs()); + } + else + v.set_instance_scaling_factor(scale); + } + } + else if (is_single_volume() || is_single_modifier()) + v.set_volume_scaling_factor(scale); + else { + const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scale); + if (m_mode == Instance) { + const Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_instance_scale_matrix()).matrix().block(0, 0, 3, 3); + // extracts scaling factors from the composed transformation + const Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); + if (transformation_type.joint()) + v.set_instance_offset(m_cache.dragging_center + m * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); + + v.set_instance_scaling_factor(new_scale); + } + else if (m_mode == Volume) { + const Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_volume_scale_matrix()).matrix().block(0, 0, 3, 3); + // extracts scaling factors from the composed transformation + const Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); + if (transformation_type.joint()) { + const Vec3d offset = m * (m_cache.volumes_data[i].get_volume_position() + m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center); + v.set_volume_offset(m_cache.dragging_center - m_cache.volumes_data[i].get_instance_position() + offset); + } + v.set_volume_scaling_factor(new_scale); + } + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + ensure_on_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::scale_to_fit_print_volume(const BuildVolume& volume) +{ + auto fit = [this](double s, Vec3d offset) { + if (s <= 0.0 || s == 1.0) + return; + + wxGetApp().plater()->take_snapshot(_L("Scale To Fit")); + + TransformationType type; + type.set_world(); + type.set_relative(); + type.set_joint(); + + // apply scale + setup_cache(); + scale(s * Vec3d::Ones(), type); + wxGetApp().plater()->canvas3D()->do_scale(""); // avoid storing another snapshot + + // center selection on print bed + setup_cache(); + offset.z() = -get_bounding_box().min.z(); +#if ENABLE_WORLD_COORDINATE + TransformationType trafo_type; + trafo_type.set_relative(); + translate(offset, trafo_type); +#else + translate(offset); +#endif // ENABLE_WORLD_COORDINATE + wxGetApp().plater()->canvas3D()->do_move(""); // avoid storing another snapshot + + wxGetApp().obj_manipul()->set_dirty(); + }; + + auto fit_rectangle = [this, fit](const BuildVolume& volume) { + const BoundingBoxf3 print_volume = volume.bounding_volume(); + const Vec3d print_volume_size = print_volume.size(); + + // adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings + const Vec3d box_size = get_bounding_box().size() + 0.02 * Vec3d::Ones(); + + const double sx = (box_size.x() != 0.0) ? print_volume_size.x() / box_size.x() : 0.0; + const double sy = (box_size.y() != 0.0) ? print_volume_size.y() / box_size.y() : 0.0; + const double sz = (box_size.z() != 0.0) ? print_volume_size.z() / box_size.z() : 0.0; + + if (sx != 0.0 && sy != 0.0 && sz != 0.0) + fit(std::min(sx, std::min(sy, sz)), print_volume.center() - get_bounding_box().center()); + }; + + auto fit_circle = [this, fit](const BuildVolume& volume) { + const Geometry::Circled& print_circle = volume.circle(); + double print_circle_radius = unscale(print_circle.radius); + + if (print_circle_radius == 0.0) + return; + + Points points; + double max_z = 0.0; + for (unsigned int i : m_list) { + const GLVolume& v = *(*m_volumes)[i]; + TriangleMesh hull_3d = *v.convex_hull(); + hull_3d.transform(v.world_matrix()); + max_z = std::max(max_z, hull_3d.bounding_box().size().z()); + const Polygon hull_2d = hull_3d.convex_hull(); + points.insert(points.end(), hull_2d.begin(), hull_2d.end()); + } + + if (points.empty()) + return; + + const Geometry::Circled circle = Geometry::smallest_enclosing_circle_welzl(points); + // adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings + const double circle_radius = unscale(circle.radius) + 0.01; + + if (circle_radius == 0.0 || max_z == 0.0) + return; + + const double s = std::min(print_circle_radius / circle_radius, volume.max_print_height() / max_z); + const Vec3d sel_center = get_bounding_box().center(); + const Vec3d offset = s * (Vec3d(unscale(circle.center.x()), unscale(circle.center.y()), 0.5 * max_z) - sel_center); + const Vec3d print_center = { unscale(print_circle.center.x()), unscale(print_circle.center.y()), 0.5 * volume.max_print_height() }; + fit(s, print_center - (sel_center + offset)); + }; + + if (is_empty() || m_mode == Volume) + return; + + switch (volume.type()) + { + case BuildVolume::Type::Rectangle: { fit_rectangle(volume); break; } + case BuildVolume::Type::Circle: { fit_circle(volume); break; } + default: { break; } + } +} + +void Selection::mirror(Axis axis) +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + if (is_single_full_instance()) + v.set_instance_mirror(axis, -v.get_instance_mirror(axis)); + else if (m_mode == Volume) + v.set_volume_mirror(axis, -v.get_volume_mirror(axis)); + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + set_bounding_boxes_dirty(); +} + +#if ENABLE_WORLD_COORDINATE +void Selection::scale_and_translate(const Vec3d& scale, const Vec3d& translation, TransformationType transformation_type) +{ + if (!m_valid) + return; + + Vec3d relative_scale = scale; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + + if (transformation_type.absolute()) { + // convert from absolute scaling to relative scaling + BoundingBoxf3 original_box; + if (m_mode == Instance) { + assert(is_from_fully_selected_instance(i)); + if (transformation_type.world()) + original_box = get_full_unscaled_instance_bounding_box(); + else + original_box = get_full_unscaled_instance_local_bounding_box(); + } + else { + if (transformation_type.world()) + original_box = v.transformed_convex_hull_bounding_box((volume_data.get_instance_transform() * + volume_data.get_volume_transform()).get_matrix_no_scaling_factor()); + else if (transformation_type.instance()) + original_box = v.transformed_convex_hull_bounding_box(volume_data.get_volume_transform().get_matrix_no_scaling_factor()); + else + original_box = v.bounding_box(); + } + + relative_scale = original_box.size().cwiseProduct(scale).cwiseQuotient(m_box.get_bounding_box().size()); + } + + if (m_mode == Instance) { + assert(is_from_fully_selected_instance(i)); + if (transformation_type.world()) { + const Transform3d scale_matrix = Geometry::scale_transform(relative_scale); + const Transform3d offset_matrix = (transformation_type.joint() && translation.isApprox(Vec3d::Zero())) ? + // non-constrained scaling - add offset to scale around selection center + Geometry::translation_transform(m_cache.dragging_center + scale_matrix * (inst_trafo.get_offset() - m_cache.dragging_center)) : + // constrained scaling - add offset to keep constraint + Geometry::translation_transform(translation) * inst_trafo.get_offset_matrix(); + v.set_instance_transformation(offset_matrix * scale_matrix * inst_trafo.get_matrix_no_offset()); + } + else if (transformation_type.local()) { + const Transform3d scale_matrix = Geometry::scale_transform(relative_scale); + Vec3d offset; + if (transformation_type.joint() && translation.isApprox(Vec3d::Zero())) { + // non-constrained scaling - add offset to scale around selection center + offset = inst_trafo.get_matrix_no_offset().inverse() * (inst_trafo.get_offset() - m_cache.dragging_center); + offset = inst_trafo.get_matrix_no_offset() * (scale_matrix * offset - offset); + } + else + // constrained scaling - add offset to keep constraint + offset = translation; + + v.set_instance_transformation(Geometry::translation_transform(offset) * inst_trafo.get_matrix() * scale_matrix); + } + else + assert(false); + } + else + transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(relative_scale)); + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + ensure_on_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} + +void Selection::reset_skew() +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + Geometry::Transformation inst_trafo = volume_data.get_instance_transform(); + Geometry::Transformation vol_trafo = volume_data.get_volume_transform(); + Geometry::Transformation world_trafo = inst_trafo * vol_trafo; + if (world_trafo.has_skew()) { + if (!inst_trafo.has_skew() && !vol_trafo.has_skew()) { + // = [I][V] + world_trafo.reset_offset(); + world_trafo.reset_skew(); + v.set_volume_transformation(vol_trafo.get_offset_matrix() * inst_trafo.get_matrix_no_offset().inverse() * world_trafo.get_matrix()); + } + else { + // = + // = [V] + // = [I] + if (inst_trafo.has_skew()) { + inst_trafo.reset_skew(); + v.set_instance_transformation(inst_trafo); + } + if (vol_trafo.has_skew()) { + vol_trafo.reset_skew(); + v.set_volume_transformation(vol_trafo); + } + } + } + else { + // [W] = [I][V] + // [W] = + if (inst_trafo.has_skew()) { + inst_trafo.reset_skew(); + v.set_instance_transformation(inst_trafo); + } + if (vol_trafo.has_skew()) { + vol_trafo.reset_skew(); + v.set_volume_transformation(vol_trafo); + } + } + } + + ensure_on_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#else +void Selection::translate(unsigned int object_idx, const Vec3d& displacement) +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + if (v.object_idx() == (int)object_idx) + v.set_instance_offset(v.get_instance_offset() + displacement); + } + + std::set done; // prevent processing volumes twice + done.insert(m_list.begin(), m_list.end()); + + for (unsigned int i : m_list) { + if (done.size() == m_volumes->size()) + break; + + if ((*m_volumes)[i]->is_wipe_tower) + continue; + + int object_idx = (*m_volumes)[i]->object_idx(); + + // Process unselected volumes of the object. + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (done.size() == m_volumes->size()) + break; + + if (done.find(j) != done.end()) + continue; + + GLVolume& v = *(*m_volumes)[j]; + if (v.object_idx() != object_idx) + continue; + + v.set_instance_offset(v.get_instance_offset() + displacement); + done.insert(j); + } + } + + this->set_bounding_boxes_dirty(); +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::translate(unsigned int object_idx, unsigned int instance_idx, const Vec3d& displacement) +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + if (v.object_idx() == (int)object_idx && v.instance_idx() == (int)instance_idx) +#if ENABLE_WORLD_COORDINATE + v.set_instance_transformation(Geometry::translation_transform(displacement) * v.get_instance_transformation().get_matrix()); +#else + v.set_instance_offset(v.get_instance_offset() + displacement); +#endif // ENABLE_WORLD_COORDINATE + } + + std::set done; // prevent processing volumes twice + done.insert(m_list.begin(), m_list.end()); + + for (unsigned int i : m_list) { + if (done.size() == m_volumes->size()) + break; + + if ((*m_volumes)[i]->is_wipe_tower) + continue; + + const int object_idx = (*m_volumes)[i]->object_idx(); + + // Process unselected volumes of the object. + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (done.size() == m_volumes->size()) + break; + + if (done.find(j) != done.end()) + continue; + + GLVolume& v = *(*m_volumes)[j]; + if (v.object_idx() != object_idx || v.instance_idx() != (int)instance_idx) + continue; + +#if ENABLE_WORLD_COORDINATE + v.set_instance_transformation(Geometry::translation_transform(displacement) * v.get_instance_transformation().get_matrix()); +#else + v.set_instance_offset(v.get_instance_offset() + displacement); +#endif // ENABLE_WORLD_COORDINATE + done.insert(j); + } + } + + this->set_bounding_boxes_dirty(); +} + +#if ENABLE_WORLD_COORDINATE +int Selection::bake_transform_if_needed() const +{ + if ((is_single_full_instance() && wxGetApp().obj_manipul()->is_world_coordinates()) || + (is_single_volume_or_modifier() && !wxGetApp().obj_manipul()->is_local_coordinates())) { + // Verify whether the instance rotation is multiples of 90 degrees, so that the scaling in world coordinates is possible. + // all volumes in the selection belongs to the same instance, any of them contains the needed instance data, so we take the first one + const GLVolume& volume = *get_first_volume(); + bool needs_baking = false; + if (is_single_full_instance()) { + // Is the instance angle close to a multiple of 90 degrees? + needs_baking |= !Geometry::is_rotation_ninety_degrees(volume.get_instance_rotation()); + // Are all volumes angles close to a multiple of 90 degrees? + for (unsigned int id : get_volume_idxs()) { + if (needs_baking) + break; + needs_baking |= !Geometry::is_rotation_ninety_degrees(get_volume(id)->get_volume_rotation()); + } + } + else if (is_single_volume_or_modifier()) { + // is the volume angle close to a multiple of 90 degrees? + needs_baking |= !Geometry::is_rotation_ninety_degrees(volume.get_volume_rotation()); + if (wxGetApp().obj_manipul()->is_world_coordinates()) + // Is the instance angle close to a multiple of 90 degrees? + needs_baking |= !Geometry::is_rotation_ninety_degrees(volume.get_instance_rotation()); + } + + if (needs_baking) { + MessageDialog dlg((wxWindow*)wxGetApp().mainframe, + _L("The currently manipulated object is tilted or contains tilted parts (rotation angles are not multiples of 90°). " + "Non-uniform scaling of tilted objects is only possible in non-local coordinate systems, " + "once the rotation is embedded into the object coordinates.") + "\n" + + _L("This operation is irreversible.") + "\n" + + _L("Do you want to proceed?"), + SLIC3R_APP_NAME, + wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION); + if (dlg.ShowModal() != wxID_YES) + return -1; + + wxGetApp().plater()->take_snapshot(_("Bake transform")); + + // Bake the rotation into the meshes of the object. + wxGetApp().model().objects[volume.composite_id.object_id]->bake_xy_rotation_into_meshes(volume.composite_id.instance_id); + // Update the 3D scene, selections etc. + wxGetApp().plater()->update(); + return 0; + } + } + + return 1; +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::erase() +{ + if (!m_valid) + return; + + if (is_single_full_object()) + wxGetApp().obj_list()->delete_from_model_and_list(ItemType::itObject, get_object_idx(), 0); + else if (is_multiple_full_object()) { + std::vector items; + items.reserve(m_cache.content.size()); + for (ObjectIdxsToInstanceIdxsMap::iterator it = m_cache.content.begin(); it != m_cache.content.end(); ++it) { + items.emplace_back(ItemType::itObject, it->first, 0); + } + wxGetApp().obj_list()->delete_from_model_and_list(items); + } + else if (is_multiple_full_instance()) { + std::set> instances_idxs; + for (ObjectIdxsToInstanceIdxsMap::iterator obj_it = m_cache.content.begin(); obj_it != m_cache.content.end(); ++obj_it) { + for (InstanceIdxsList::reverse_iterator inst_it = obj_it->second.rbegin(); inst_it != obj_it->second.rend(); ++inst_it) { + instances_idxs.insert(std::make_pair(obj_it->first, *inst_it)); + } + } + + std::vector items; + items.reserve(instances_idxs.size()); + for (const std::pair& i : instances_idxs) { + items.emplace_back(ItemType::itInstance, i.first, i.second); + } + wxGetApp().obj_list()->delete_from_model_and_list(items); + } + else if (is_single_full_instance()) + wxGetApp().obj_list()->delete_from_model_and_list(ItemType::itInstance, get_object_idx(), get_instance_idx()); + else if (is_mixed()) { + std::set items_set; + std::map volumes_in_obj; + + for (auto i : m_list) { + const auto gl_vol = (*m_volumes)[i]; + const auto glv_obj_idx = gl_vol->object_idx(); + const auto model_object = m_model->objects[glv_obj_idx]; + + if (model_object->instances.size() == 1) { + if (model_object->volumes.size() == 1) + items_set.insert(ItemForDelete(ItemType::itObject, glv_obj_idx, -1)); + else { + items_set.insert(ItemForDelete(ItemType::itVolume, glv_obj_idx, gl_vol->volume_idx())); + int idx = (volumes_in_obj.find(glv_obj_idx) == volumes_in_obj.end()) ? 0 : volumes_in_obj.at(glv_obj_idx); + volumes_in_obj[glv_obj_idx] = ++idx; + } + continue; + } + + const auto glv_ins_idx = gl_vol->instance_idx(); + + for (auto obj_ins : m_cache.content) { + if (obj_ins.first == glv_obj_idx) { + if (obj_ins.second.find(glv_ins_idx) != obj_ins.second.end()) { + if (obj_ins.second.size() == model_object->instances.size()) + items_set.insert(ItemForDelete(ItemType::itObject, glv_obj_idx, -1)); + else + items_set.insert(ItemForDelete(ItemType::itInstance, glv_obj_idx, glv_ins_idx)); + + break; + } + } + } + } + + std::vector items; + items.reserve(items_set.size()); + for (const ItemForDelete& i : items_set) { + if (i.type == ItemType::itVolume) { + const int vol_in_obj_cnt = volumes_in_obj.find(i.obj_idx) == volumes_in_obj.end() ? 0 : volumes_in_obj.at(i.obj_idx); + if (vol_in_obj_cnt == (int)m_model->objects[i.obj_idx]->volumes.size()) { + if (i.sub_obj_idx == vol_in_obj_cnt - 1) + items.emplace_back(ItemType::itObject, i.obj_idx, 0); + continue; + } + } + items.emplace_back(i.type, i.obj_idx, i.sub_obj_idx); + } + + wxGetApp().obj_list()->delete_from_model_and_list(items); + } + else { + std::set> volumes_idxs; + for (unsigned int i : m_list) { + const GLVolume* v = (*m_volumes)[i]; + // Only remove volumes associated with ModelVolumes from the object list. + // Temporary meshes (SLA supports or pads) are not managed by the object list. + if (v->volume_idx() >= 0) + volumes_idxs.insert(std::make_pair(v->object_idx(), v->volume_idx())); + } + + std::vector items; + items.reserve(volumes_idxs.size()); + for (const std::pair& v : volumes_idxs) { + items.emplace_back(ItemType::itVolume, v.first, v.second); + } + + wxGetApp().obj_list()->delete_from_model_and_list(items); + ensure_not_below_bed(); + } +} + +void Selection::render(float scale_factor) +{ + if (!m_valid || is_empty()) + return; + + m_scale_factor = scale_factor; + // render cumulative bounding box of selected volumes +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + BoundingBoxf3 box; + Transform3d trafo; + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) { + box = get_bounding_box(); + trafo = Transform3d::Identity(); + } + else if (coordinates_type == ECoordinatesType::Local && is_single_volume_or_modifier()) { + const GLVolume& v = *get_first_volume(); + box = v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_scaling_factor_matrix()); + trafo = v.get_instance_transformation().get_matrix() * v.get_volume_transformation().get_matrix_no_scaling_factor(); + } + else { + const Selection::IndicesList& ids = get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume& v = *get_volume(id); + box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); + } + const Geometry::Transformation inst_trafo = get_first_volume()->get_instance_transformation(); + box = box.transformed(inst_trafo.get_scaling_factor_matrix()); + trafo = inst_trafo.get_matrix_no_scaling_factor(); + } + + render_bounding_box(box, trafo, ColorRGB::WHITE()); +#else + render_bounding_box(get_bounding_box(), ColorRGB::WHITE()); +#endif // ENABLE_WORLD_COORDINATE +#else + render_selected_volumes(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + render_synchronized_volumes(); +} + +#if ENABLE_RENDER_SELECTION_CENTER +void Selection::render_center(bool gizmo_is_dragging) +{ + if (!m_valid || is_empty()) + return; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + shader->start_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + const Vec3d center = gizmo_is_dragging ? m_cache.dragging_center : get_bounding_box().center(); + + glsafe(::glDisable(GL_DEPTH_TEST)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d view_model_matrix = camera.get_view_matrix() * Geometry::assemble_transform(center); + + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + m_vbo_sphere.set_color(ColorRGBA::WHITE()); +#else + glsafe(::glPushMatrix()); + glsafe(::glTranslated(center.x(), center.y(), center.z())); + m_vbo_sphere.set_color(-1, ColorRGBA::WHITE()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + m_vbo_sphere.render(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); +#else + glsafe(::glPopMatrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} +#endif // ENABLE_RENDER_SELECTION_CENTER + +void Selection::render_sidebar_hints(const std::string& sidebar_field) +{ + if (sidebar_field.empty()) + return; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader(boost::starts_with(sidebar_field, "layer") ? "flat" : "gouraud_light"); + if (shader == nullptr) + return; + + shader->start_using(); +#else + GLShaderProgram* shader = nullptr; + + if (!boost::starts_with(sidebar_field, "layer")) { + shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + + shader->start_using(); + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_DEPTH_TEST)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Transform3d base_matrix = Geometry::translation_transform(get_bounding_box().center()); + Transform3d orient_matrix = Transform3d::Identity(); +#else + glsafe(::glPushMatrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE + const Vec3d center = get_bounding_box().center(); + Vec3d axes_center = center; +#endif // ENABLE_WORLD_COORDINATE + + if (!boost::starts_with(sidebar_field, "layer")) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->set_uniform("emission_factor", 0.05f); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE + const Vec3d& center = get_bounding_box().center(); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE + if (is_single_full_instance() && !wxGetApp().obj_manipul()->is_world_coordinates()) { +#else + if (is_single_full_instance() && !wxGetApp().obj_manipul()->get_world_coordinates()) { +#endif // ENABLE_WORLD_COORDINATE +#if !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE + glsafe(::glTranslated(center.x(), center.y(), center.z())); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); + axes_center = (*m_volumes)[*m_list.begin()]->get_instance_offset(); +#else + if (!boost::starts_with(sidebar_field, "position")) { +#if !ENABLE_LEGACY_OPENGL_REMOVAL + Transform3d orient_matrix = Transform3d::Identity(); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + if (boost::starts_with(sidebar_field, "scale")) + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + else if (boost::starts_with(sidebar_field, "rotation")) { + if (boost::ends_with(sidebar_field, "x")) + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + else if (boost::ends_with(sidebar_field, "y")) { + const Vec3d& rotation = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation(); + if (rotation.x() == 0.0) + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + else + orient_matrix.rotate(Eigen::AngleAxisd(rotation.z(), Vec3d::UnitZ())); + } + } +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glMultMatrixd(orient_matrix.data())); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + } +#endif // ENABLE_WORLD_COORDINATE + } +#if ENABLE_WORLD_COORDINATE + else if (is_single_volume_or_modifier()) { +#else + else if (is_single_volume() || is_single_modifier()) { +#endif // ENABLE_WORLD_COORDINATE +#if !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE + glsafe(::glTranslated(center.x(), center.y(), center.z())); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL&& !ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + if (wxGetApp().obj_manipul()->is_local_coordinates()) { + const GLVolume* v = (*m_volumes)[*m_list.begin()]; + orient_matrix = v->get_instance_transformation().get_rotation_matrix() * v->get_volume_transformation().get_rotation_matrix(); + axes_center = (*m_volumes)[*m_list.begin()]->world_matrix().translation(); + } + else { + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); + axes_center = (*m_volumes)[*m_list.begin()]->get_instance_offset(); + } + } +#else +#if ENABLE_LEGACY_OPENGL_REMOVAL + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); +#else + glsafe(::glTranslated(center.x(), center.y(), center.z())); + Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + if (!boost::starts_with(sidebar_field, "position")) + orient_matrix = orient_matrix * (*m_volumes)[*m_list.begin()]->get_volume_transformation().get_matrix(true, false, true, true); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glMultMatrixd(orient_matrix.data())); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +#endif // ENABLE_WORLD_COORDINATE + } + else { +#if ENABLE_LEGACY_OPENGL_REMOVAL|| ENABLE_WORLD_COORDINATE + if (requires_local_axes()) +#if ENABLE_WORLD_COORDINATE + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); +#else + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); +#endif // ENABLE_WORLD_COORDINATE +#else + glsafe(::glTranslated(center.x(), center.y(), center.z())); + if (requires_local_axes()) { + const Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + glsafe(::glMultMatrixd(orient_matrix.data())); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL|| ENABLE_WORLD_COORDINATE + } + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!boost::starts_with(sidebar_field, "layer")) + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE + if (!boost::starts_with(sidebar_field, "layer")) { + shader->set_uniform("emission_factor", 0.1f); +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPushMatrix()); + glsafe(::glTranslated(center.x(), center.y(), center.z())); + glsafe(::glMultMatrixd(orient_matrix.data())); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + } +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (boost::starts_with(sidebar_field, "position")) + render_sidebar_position_hints(sidebar_field, *shader, base_matrix * orient_matrix); + else if (boost::starts_with(sidebar_field, "rotation")) + render_sidebar_rotation_hints(sidebar_field, *shader, base_matrix * orient_matrix); + else if (boost::starts_with(sidebar_field, "scale") || boost::starts_with(sidebar_field, "size")) + render_sidebar_scale_hints(sidebar_field, *shader, base_matrix * orient_matrix); + else if (boost::starts_with(sidebar_field, "layer")) + render_sidebar_layers_hints(sidebar_field, *shader); + +#if ENABLE_WORLD_COORDINATE + if (!boost::starts_with(sidebar_field, "layer")) { + if (!wxGetApp().obj_manipul()->is_world_coordinates()) + m_axes.render(Geometry::translation_transform(axes_center) * orient_matrix, 0.25f); + } +#endif // ENABLE_WORLD_COORDINATE +#else + if (boost::starts_with(sidebar_field, "position")) + render_sidebar_position_hints(sidebar_field); + else if (boost::starts_with(sidebar_field, "rotation")) + render_sidebar_rotation_hints(sidebar_field); + else if (boost::starts_with(sidebar_field, "scale") || boost::starts_with(sidebar_field, "size")) + render_sidebar_scale_hints(sidebar_field); + else if (boost::starts_with(sidebar_field, "layer")) + render_sidebar_layers_hints(sidebar_field); + +#if ENABLE_WORLD_COORDINATE + if (!boost::starts_with(sidebar_field, "layer")) { + glsafe(::glPopMatrix()); + glsafe(::glPushMatrix()); + glsafe(::glTranslated(axes_center.x(), axes_center.y(), axes_center.z())); + glsafe(::glMultMatrixd(orient_matrix.data())); + if (!wxGetApp().obj_manipul()->is_world_coordinates()) + m_axes.render(0.25f); + glsafe(::glPopMatrix()); + } +#endif // ENABLE_WORLD_COORDINATE +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +#endif // ENABLE_WORLD_COORDINATE + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + if (!boost::starts_with(sidebar_field, "layer")) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); +} + +bool Selection::requires_local_axes() const +{ + return m_mode == Volume && is_from_single_instance(); +} + +void Selection::copy_to_clipboard() +{ + if (!m_valid) + return; + + m_clipboard.reset(); + + for (const ObjectIdxsToInstanceIdxsMap::value_type& object : m_cache.content) { + ModelObject* src_object = m_model->objects[object.first]; + ModelObject* dst_object = m_clipboard.add_object(); + dst_object->name = src_object->name; + dst_object->input_file = src_object->input_file; + dst_object->config.assign_config(src_object->config); + dst_object->sla_support_points = src_object->sla_support_points; + dst_object->sla_points_status = src_object->sla_points_status; + dst_object->sla_drain_holes = src_object->sla_drain_holes; + dst_object->layer_config_ranges = src_object->layer_config_ranges; // #ys_FIXME_experiment + dst_object->layer_height_profile.assign(src_object->layer_height_profile); + dst_object->origin_translation = src_object->origin_translation; + + for (int i : object.second) { + dst_object->add_instance(*src_object->instances[i]); + } + + for (unsigned int i : m_list) { + // Copy the ModelVolumes only for the selected GLVolumes of the 1st selected instance. + const GLVolume* volume = (*m_volumes)[i]; + if (volume->object_idx() == object.first && volume->instance_idx() == *object.second.begin()) { + int volume_idx = volume->volume_idx(); + if (0 <= volume_idx && volume_idx < (int)src_object->volumes.size()) { + ModelVolume* src_volume = src_object->volumes[volume_idx]; + ModelVolume* dst_volume = dst_object->add_volume(*src_volume); + dst_volume->set_new_unique_id(); + } + else + assert(false); + } + } + } + + m_clipboard.set_mode(m_mode); +} + +void Selection::paste_from_clipboard() +{ + if (!m_valid || m_clipboard.is_empty()) + return; + + switch (m_clipboard.get_mode()) + { + case Volume: + { + if (is_from_single_instance()) + paste_volumes_from_clipboard(); + + break; + } + case Instance: + { + if (m_mode == Instance) + paste_objects_from_clipboard(); + + break; + } + } +} + +std::vector Selection::get_volume_idxs_from_object(unsigned int object_idx) const +{ + std::vector idxs; + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + if ((*m_volumes)[i]->object_idx() == (int)object_idx) + idxs.push_back(i); + } + + return idxs; +} + +std::vector Selection::get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const +{ + std::vector idxs; + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + const GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx) + idxs.push_back(i); + } + + return idxs; +} + +std::vector Selection::get_volume_idxs_from_volume(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx) const +{ + std::vector idxs; + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) + { + const GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx && v->volume_idx() == (int)volume_idx) { + if ((int)instance_idx != -1 && v->instance_idx() == (int)instance_idx) + idxs.push_back(i); + } + } + + return idxs; +} + +std::vector Selection::get_missing_volume_idxs_from(const std::vector& volume_idxs) const +{ + std::vector idxs; + + for (unsigned int i : m_list) { + std::vector::const_iterator it = std::find(volume_idxs.begin(), volume_idxs.end(), i); + if (it == volume_idxs.end()) + idxs.push_back(i); + } + + return idxs; +} + +std::vector Selection::get_unselected_volume_idxs_from(const std::vector& volume_idxs) const +{ + std::vector idxs; + + for (unsigned int i : volume_idxs) { + if (m_list.find(i) == m_list.end()) + idxs.push_back(i); + } + + return idxs; +} + +void Selection::update_valid() +{ + m_valid = (m_volumes != nullptr) && (m_model != nullptr); +} + +void Selection::update_type() +{ + m_cache.content.clear(); + m_type = Mixed; + + for (unsigned int i : m_list) { + const GLVolume* volume = (*m_volumes)[i]; + int obj_idx = volume->object_idx(); + int inst_idx = volume->instance_idx(); + ObjectIdxsToInstanceIdxsMap::iterator obj_it = m_cache.content.find(obj_idx); + if (obj_it == m_cache.content.end()) + obj_it = m_cache.content.insert(ObjectIdxsToInstanceIdxsMap::value_type(obj_idx, InstanceIdxsList())).first; + + obj_it->second.insert(inst_idx); + } + + bool requires_disable = false; + + if (!m_valid) + m_type = Invalid; + else + { + if (m_list.empty()) + m_type = Empty; + else if (m_list.size() == 1) { + const GLVolume* first = (*m_volumes)[*m_list.begin()]; + if (first->is_wipe_tower) + m_type = WipeTower; + else if (first->is_modifier) { + m_type = SingleModifier; + requires_disable = true; + } + else { + const ModelObject* model_object = m_model->objects[first->object_idx()]; + unsigned int volumes_count = (unsigned int)model_object->volumes.size(); + unsigned int instances_count = (unsigned int)model_object->instances.size(); + if (volumes_count * instances_count == 1) { + m_type = SingleFullObject; + // ensures the correct mode is selected + m_mode = Instance; + } + else if (volumes_count == 1) // instances_count > 1 + { + m_type = SingleFullInstance; + // ensures the correct mode is selected + m_mode = Instance; + } + else { + m_type = SingleVolume; + requires_disable = true; + } + } + } + else { + unsigned int sla_volumes_count = 0; + // Note: sla_volumes_count is a count of the selected sla_volumes per object instead of per instance, like a model_volumes_count is + for (unsigned int i : m_list) { + if ((*m_volumes)[i]->volume_idx() < 0) + ++sla_volumes_count; + } + + if (m_cache.content.size() == 1) // single object + { + const ModelObject* model_object = m_model->objects[m_cache.content.begin()->first]; + unsigned int model_volumes_count = (unsigned int)model_object->volumes.size(); + + unsigned int instances_count = (unsigned int)model_object->instances.size(); + unsigned int selected_instances_count = (unsigned int)m_cache.content.begin()->second.size(); + if (model_volumes_count * instances_count + sla_volumes_count == (unsigned int)m_list.size()) { + m_type = SingleFullObject; + // ensures the correct mode is selected + m_mode = Instance; + } + else if (selected_instances_count == 1) { + if (model_volumes_count + sla_volumes_count == (unsigned int)m_list.size()) { + m_type = SingleFullInstance; + // ensures the correct mode is selected + m_mode = Instance; + } + else { + unsigned int modifiers_count = 0; + for (unsigned int i : m_list) { + if ((*m_volumes)[i]->is_modifier) + ++modifiers_count; + } + + if (modifiers_count == 0) + m_type = MultipleVolume; + else if (modifiers_count == (unsigned int)m_list.size()) + m_type = MultipleModifier; + + requires_disable = true; + } + } + else if (selected_instances_count > 1 && selected_instances_count * model_volumes_count + sla_volumes_count == (unsigned int)m_list.size()) { + m_type = MultipleFullInstance; + // ensures the correct mode is selected + m_mode = Instance; + } + } + else { + unsigned int sels_cntr = 0; + for (ObjectIdxsToInstanceIdxsMap::iterator it = m_cache.content.begin(); it != m_cache.content.end(); ++it) { + const ModelObject* model_object = m_model->objects[it->first]; + unsigned int volumes_count = (unsigned int)model_object->volumes.size(); + unsigned int instances_count = (unsigned int)model_object->instances.size(); + sels_cntr += volumes_count * instances_count; + } + if (sels_cntr + sla_volumes_count == (unsigned int)m_list.size()) { + m_type = MultipleFullObject; + // ensures the correct mode is selected + m_mode = Instance; + } + } + } + } + + int object_idx = get_object_idx(); + int instance_idx = get_instance_idx(); + for (GLVolume* v : *m_volumes) { + v->disabled = requires_disable ? (v->object_idx() != object_idx) || (v->instance_idx() != instance_idx) : false; + } + +#if ENABLE_SELECTION_DEBUG_OUTPUT + std::cout << "Selection: "; + std::cout << "mode: "; + switch (m_mode) + { + case Volume: + { + std::cout << "Volume"; + break; + } + case Instance: + { + std::cout << "Instance"; + break; + } + } + + std::cout << " - type: "; + + switch (m_type) + { + case Invalid: + { + std::cout << "Invalid" << std::endl; + break; + } + case Empty: + { + std::cout << "Empty" << std::endl; + break; + } + case WipeTower: + { + std::cout << "WipeTower" << std::endl; + break; + } + case SingleModifier: + { + std::cout << "SingleModifier" << std::endl; + break; + } + case MultipleModifier: + { + std::cout << "MultipleModifier" << std::endl; + break; + } + case SingleVolume: + { + std::cout << "SingleVolume" << std::endl; + break; + } + case MultipleVolume: + { + std::cout << "MultipleVolume" << std::endl; + break; + } + case SingleFullObject: + { + std::cout << "SingleFullObject" << std::endl; + break; + } + case MultipleFullObject: + { + std::cout << "MultipleFullObject" << std::endl; + break; + } + case SingleFullInstance: + { + std::cout << "SingleFullInstance" << std::endl; + break; + } + case MultipleFullInstance: + { + std::cout << "MultipleFullInstance" << std::endl; + break; + } + case Mixed: + { + std::cout << "Mixed" << std::endl; + break; + } + } +#endif // ENABLE_SELECTION_DEBUG_OUTPUT +} + +void Selection::set_caches() +{ + m_cache.volumes_data.clear(); + m_cache.sinking_volumes.clear(); + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + const GLVolume& v = *(*m_volumes)[i]; + m_cache.volumes_data.emplace(i, VolumeCache(v.get_volume_transformation(), v.get_instance_transformation())); + if (v.is_sinking()) + m_cache.sinking_volumes.push_back(i); + } + m_cache.dragging_center = get_bounding_box().center(); +} + +void Selection::do_add_volume(unsigned int volume_idx) +{ + m_list.insert(volume_idx); + GLVolume* v = (*m_volumes)[volume_idx]; + v->selected = true; + if (v->hover == GLVolume::HS_Select || v->hover == GLVolume::HS_Deselect) + v->hover = GLVolume::HS_Hover; +} + +void Selection::do_add_volumes(const std::vector& volume_idxs) +{ + for (unsigned int i : volume_idxs) + { + if (i < (unsigned int)m_volumes->size()) + do_add_volume(i); + } +} + +void Selection::do_remove_volume(unsigned int volume_idx) +{ + IndicesList::iterator v_it = m_list.find(volume_idx); + if (v_it == m_list.end()) + return; + + m_list.erase(v_it); + + (*m_volumes)[volume_idx]->selected = false; +} + +void Selection::do_remove_instance(unsigned int object_idx, unsigned int instance_idx) +{ + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx) + do_remove_volume(i); + } +} + +void Selection::do_remove_object(unsigned int object_idx) +{ + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx) + do_remove_volume(i); + } +} + +#if !ENABLE_LEGACY_OPENGL_REMOVAL +void Selection::render_selected_volumes() const +{ + float color[3] = { 1.0f, 1.0f, 1.0f }; + render_bounding_box(get_bounding_box(), color); +} +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +void Selection::render_synchronized_volumes() +{ + if (m_mode == Instance) + return; + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + float color[3] = { 1.0f, 1.0f, 0.0f }; +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + BoundingBoxf3 box; + Transform3d trafo; +#endif // ENABLE_WORLD_COORDINATE + + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + int object_idx = volume.object_idx(); + int volume_idx = volume.volume_idx(); + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (i == j) + continue; + + const GLVolume& v = *(*m_volumes)[j]; + if (v.object_idx() != object_idx || v.volume_idx() != volume_idx) + continue; + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + if (coordinates_type == ECoordinatesType::World) { + box = v.transformed_convex_hull_bounding_box(); + trafo = Transform3d::Identity(); + } + else if (coordinates_type == ECoordinatesType::Local) { + box = v.bounding_box(); + trafo = v.world_matrix(); + } + else { + box = v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix()); + trafo = v.get_instance_transformation().get_matrix(); + } + render_bounding_box(box, trafo, ColorRGB::YELLOW()); +#else + render_bounding_box(v.transformed_convex_hull_bounding_box(), ColorRGB::YELLOW()); +#endif // ENABLE_WORLD_COORDINATE +#else + render_bounding_box(v.transformed_convex_hull_bounding_box(), color); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE +void Selection::render_bounding_box(const BoundingBoxf3& box, const Transform3d& trafo, const ColorRGB& color) +#else +void Selection::render_bounding_box(const BoundingBoxf3& box, const ColorRGB& color) +#endif // ENABLE_WORLD_COORDINATE +{ +#else +void Selection::render_bounding_box(const BoundingBoxf3 & box, float* color) const +{ + if (color == nullptr) + return; + + const Vec3f b_min = box.min.cast(); + const Vec3f b_max = box.max.cast(); + const Vec3f size = 0.2f * box.size().cast(); + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glColor3fv(color)); + glsafe(::glLineWidth(2.0f * m_scale_factor)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const BoundingBoxf3& curr_box = m_box.get_bounding_box(); + + if (!m_box.is_initialized() || !is_approx(box.min, curr_box.min) || !is_approx(box.max, curr_box.max)) { + m_box.reset(); + + const Vec3f b_min = box.min.cast(); + const Vec3f b_max = box.max.cast(); + const Vec3f size = 0.2f * box.size().cast(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(48); + init_data.reserve_indices(48); + + // vertices + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y() + size.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z() + size.z())); + + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y() + size.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z() + size.z())); + + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y() - size.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z() + size.z())); + + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y() - size.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z() + size.z())); + + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y() + size.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z() - size.z())); + + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y() + size.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z() - size.z())); + + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y() - size.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z() - size.z())); + + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y() - size.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z() - size.z())); + + // indices + for (unsigned int i = 0; i < 48; ++i) { + init_data.add_index(i); + } + + m_box.init_from(std::move(init_data)); + } + + glsafe(::glEnable(GL_DEPTH_TEST)); + +#if ENABLE_GL_CORE_PROFILE + if (!OpenGLManager::get_gl_info().is_core_profile()) + glsafe(::glLineWidth(2.0f * m_scale_factor)); + + GLShaderProgram* shader = OpenGLManager::get_gl_info().is_core_profile() ? wxGetApp().get_shader("dashed_thick_lines") : wxGetApp().get_shader("flat"); +#else + glsafe(::glLineWidth(2.0f * m_scale_factor)); + GLShaderProgram* shader = wxGetApp().get_shader("flat"); +#endif // ENABLE_GL_CORE_PROFILE + if (shader == nullptr) + return; + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPushMatrix()); + glsafe(::glMultMatrixd(trafo.data())); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +#endif // ENABLE_WORLD_COORDINATE + + shader->start_using(); + const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo); +#else + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#if ENABLE_GL_CORE_PROFILE + const std::array& viewport = camera.get_viewport(); + shader->set_uniform("viewport_size", Vec2d(double(viewport[2]), double(viewport[3]))); + shader->set_uniform("width", 1.5f); + shader->set_uniform("gap_size", 0.0f); +#endif // ENABLE_GL_CORE_PROFILE + m_box.set_color(to_rgba(color)); + m_box.render(); + shader->stop_using(); + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_LEGACY_OPENGL_REMOVAL + glsafe(::glPopMatrix()); +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL +#endif // ENABLE_WORLD_COORDINATE +#else + ::glBegin(GL_LINES); + + ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0) + size(0), b_min(1), b_min(2)); + ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0), b_min(1) + size(1), b_min(2)); + ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0), b_min(1), b_min(2) + size(2)); + + ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0) - size(0), b_min(1), b_min(2)); + ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0), b_min(1) + size(1), b_min(2)); + ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0), b_min(1), b_min(2) + size(2)); + + ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0) - size(0), b_max(1), b_min(2)); + ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0), b_max(1) - size(1), b_min(2)); + ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0), b_max(1), b_min(2) + size(2)); + + ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0) + size(0), b_max(1), b_min(2)); + ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0), b_max(1) - size(1), b_min(2)); + ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0), b_max(1), b_min(2) + size(2)); + + ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0) + size(0), b_min(1), b_max(2)); + ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0), b_min(1) + size(1), b_max(2)); + ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0), b_min(1), b_max(2) - size(2)); + + ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0) - size(0), b_min(1), b_max(2)); + ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0), b_min(1) + size(1), b_max(2)); + ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0), b_min(1), b_max(2) - size(2)); + + ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0) - size(0), b_max(1), b_max(2)); + ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0), b_max(1) - size(1), b_max(2)); + ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0), b_max(1), b_max(2) - size(2)); + + ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0) + size(0), b_max(1), b_max(2)); + ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0), b_max(1) - size(1), b_max(2)); + ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0), b_max(1), b_max(2) - size(2)); + + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +static ColorRGBA get_color(Axis axis) +{ + return AXES_COLOR[axis]; +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void Selection::render_sidebar_position_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) +#else +void Selection::render_sidebar_position_hints(const std::string& sidebar_field) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d& view_matrix = camera.get_view_matrix(); + shader.set_uniform("projection_matrix", camera.get_projection_matrix()); + + if (boost::ends_with(sidebar_field, "x")) { + const Transform3d model_matrix = matrix * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()); + shader.set_uniform("view_model_matrix", view_matrix * model_matrix); + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader.set_uniform("view_normal_matrix", view_normal_matrix); + m_arrow.set_color(get_color(X)); + m_arrow.render(); + } + else if (boost::ends_with(sidebar_field, "y")) { + shader.set_uniform("view_model_matrix", view_matrix * matrix); + shader.set_uniform("view_normal_matrix", (Matrix3d)Matrix3d::Identity()); + m_arrow.set_color(get_color(Y)); + m_arrow.render(); + } + else if (boost::ends_with(sidebar_field, "z")) { + const Transform3d model_matrix = matrix * Geometry::rotation_transform(0.5 * PI * Vec3d::UnitX()); + shader.set_uniform("view_model_matrix", view_matrix * model_matrix); + const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader.set_uniform("view_normal_matrix", view_normal_matrix); + m_arrow.set_color(get_color(Z)); + m_arrow.render(); + } +#else + if (boost::ends_with(sidebar_field, "x")) { + glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); + m_arrow.set_color(-1, get_color(X)); + m_arrow.render(); + } + else if (boost::ends_with(sidebar_field, "y")) { + m_arrow.set_color(-1, get_color(Y)); + m_arrow.render(); + } + else if (boost::ends_with(sidebar_field, "z")) { + glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); + m_arrow.set_color(-1, get_color(Z)); + m_arrow.render(); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) +#else +void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + auto render_sidebar_rotation_hint = [this](GLShaderProgram& shader, const Transform3d& view_matrix, const Transform3d& model_matrix) { + shader.set_uniform("view_model_matrix", view_matrix * model_matrix); + Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader.set_uniform("view_normal_matrix", view_normal_matrix); + m_curved_arrow.render(); + const Transform3d matrix = model_matrix * Geometry::rotation_transform(PI * Vec3d::UnitZ()); + shader.set_uniform("view_model_matrix", view_matrix * matrix); + view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader.set_uniform("view_normal_matrix", view_normal_matrix); + m_curved_arrow.render(); + }; + + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d& view_matrix = camera.get_view_matrix(); + shader.set_uniform("projection_matrix", camera.get_projection_matrix()); + + if (boost::ends_with(sidebar_field, "x")) { + m_curved_arrow.set_color(get_color(X)); + render_sidebar_rotation_hint(shader, view_matrix, matrix * Geometry::rotation_transform(0.5 * PI * Vec3d::UnitY())); + } + else if (boost::ends_with(sidebar_field, "y")) { + m_curved_arrow.set_color(get_color(Y)); + render_sidebar_rotation_hint(shader, view_matrix, matrix * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitX())); + } + else if (boost::ends_with(sidebar_field, "z")) { + m_curved_arrow.set_color(get_color(Z)); + render_sidebar_rotation_hint(shader, view_matrix, matrix); + } +#else + auto render_sidebar_rotation_hint = [this]() { + m_curved_arrow.render(); + glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); + m_curved_arrow.render(); + }; + + if (boost::ends_with(sidebar_field, "x")) { + glsafe(::glRotated(90.0, 0.0, 1.0, 0.0)); + m_curved_arrow.set_color(-1, get_color(X)); + render_sidebar_rotation_hint(); + } + else if (boost::ends_with(sidebar_field, "y")) { + glsafe(::glRotated(-90.0, 1.0, 0.0, 0.0)); + m_curved_arrow.set_color(-1, get_color(Y)); + render_sidebar_rotation_hint(); + } + else if (boost::ends_with(sidebar_field, "z")) { + m_curved_arrow.set_color(-1, get_color(Z)); + render_sidebar_rotation_hint(); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void Selection::render_sidebar_scale_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) +#else +void Selection::render_sidebar_scale_hints(const std::string& sidebar_field) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ +#if ENABLE_WORLD_COORDINATE + const bool uniform_scale = wxGetApp().obj_manipul()->get_uniform_scaling(); +#else + const bool uniform_scale = requires_uniform_scale() || wxGetApp().obj_manipul()->get_uniform_scaling(); +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_LEGACY_OPENGL_REMOVAL + auto render_sidebar_scale_hint = [this, uniform_scale](Axis axis, GLShaderProgram& shader, const Transform3d& view_matrix, const Transform3d& model_matrix) { + m_arrow.set_color(uniform_scale ? UNIFORM_SCALE_COLOR : get_color(axis)); + Transform3d matrix = model_matrix * Geometry::translation_transform(5.0 * Vec3d::UnitY()); + shader.set_uniform("view_model_matrix", view_matrix * matrix); + Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader.set_uniform("view_normal_matrix", view_normal_matrix); +#else + auto render_sidebar_scale_hint = [this, uniform_scale](Axis axis) { + m_arrow.set_color(-1, uniform_scale ? UNIFORM_SCALE_COLOR : get_color(axis)); + GLShaderProgram* shader = wxGetApp().get_current_shader(); + if (shader != nullptr) + shader->set_uniform("emission_factor", 0.0f); + + glsafe(::glTranslated(0.0, 5.0, 0.0)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + m_arrow.render(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + matrix = model_matrix * Geometry::translation_transform(-5.0 * Vec3d::UnitY()) * Geometry::rotation_transform(PI * Vec3d::UnitZ()); + shader.set_uniform("view_model_matrix", view_matrix * matrix); + view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); + shader.set_uniform("view_normal_matrix", view_normal_matrix); +#else + glsafe(::glTranslated(0.0, -10.0, 0.0)); + glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + m_arrow.render(); + }; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d& view_matrix = camera.get_view_matrix(); + shader.set_uniform("projection_matrix", camera.get_projection_matrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + if (boost::ends_with(sidebar_field, "x") || uniform_scale) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + render_sidebar_scale_hint(X, shader, view_matrix, matrix * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ())); +#else + glsafe(::glPushMatrix()); + glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); + render_sidebar_scale_hint(X); + glsafe(::glPopMatrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + + if (boost::ends_with(sidebar_field, "y") || uniform_scale) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + render_sidebar_scale_hint(Y, shader, view_matrix, matrix); +#else + glsafe(::glPushMatrix()); + render_sidebar_scale_hint(Y); + glsafe(::glPopMatrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + + if (boost::ends_with(sidebar_field, "z") || uniform_scale) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + render_sidebar_scale_hint(Z, shader, view_matrix, matrix * Geometry::rotation_transform(0.5 * PI * Vec3d::UnitX())); +#else + glsafe(::glPushMatrix()); + glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); + render_sidebar_scale_hint(Z); + glsafe(::glPopMatrix()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +void Selection::render_sidebar_layers_hints(const std::string& sidebar_field, GLShaderProgram& shader) +#else +void Selection::render_sidebar_layers_hints(const std::string& sidebar_field) +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +{ + static const float Margin = 10.0f; + + std::string field = sidebar_field; + + // extract max_z + std::string::size_type pos = field.rfind("_"); + if (pos == std::string::npos) + return; + + const float max_z = float(string_to_double_decimal_point(field.substr(pos + 1))); + + // extract min_z + field = field.substr(0, pos); + pos = field.rfind("_"); + if (pos == std::string::npos) + return; + + const float min_z = float(string_to_double_decimal_point(field.substr(pos + 1))); + + // extract type + field = field.substr(0, pos); + pos = field.rfind("_"); + if (pos == std::string::npos) + return; + + const int type = std::stoi(field.substr(pos + 1)); + + const BoundingBoxf3& box = get_bounding_box(); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + const float min_x = float(box.min.x()) - Margin; + const float max_x = float(box.max.x()) + Margin; + const float min_y = float(box.min.y()) - Margin; + const float max_y = float(box.max.y()) + Margin; +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + // view dependend order of rendering to keep correct transparency + const bool camera_on_top = wxGetApp().plater()->get_camera().is_looking_downward(); + const float z1 = camera_on_top ? min_z : max_z; + const float z2 = camera_on_top ? max_z : min_z; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Vec3f p1 = { float(box.min.x()) - Margin, float(box.min.y()) - Margin, z1 }; + const Vec3f p2 = { float(box.max.x()) + Margin, float(box.max.y()) + Margin, z2 }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glDisable(GL_CULL_FACE)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_planes.models[0].is_initialized() || !is_approx(m_planes.check_points[0], p1)) { + m_planes.check_points[0] = p1; + m_planes.models[0].reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + + // vertices + init_data.add_vertex(Vec3f(p1.x(), p1.y(), z1)); + init_data.add_vertex(Vec3f(p2.x(), p1.y(), z1)); + init_data.add_vertex(Vec3f(p2.x(), p2.y(), z1)); + init_data.add_vertex(Vec3f(p1.x(), p2.y(), z1)); + + // indices + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(2, 3, 0); + + m_planes.models[0].init_from(std::move(init_data)); + } + + if (!m_planes.models[1].is_initialized() || !is_approx(m_planes.check_points[1], p2)) { + m_planes.check_points[1] = p2; + m_planes.models[1].reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + + // vertices + init_data.add_vertex(Vec3f(p1.x(), p1.y(), z2)); + init_data.add_vertex(Vec3f(p2.x(), p1.y(), z2)); + init_data.add_vertex(Vec3f(p2.x(), p2.y(), z2)); + init_data.add_vertex(Vec3f(p1.x(), p2.y(), z2)); + + // indices + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(2, 3, 0); + + m_planes.models[1].init_from(std::move(init_data)); + } + + const Camera& camera = wxGetApp().plater()->get_camera(); + shader.set_uniform("view_model_matrix", camera.get_view_matrix()); + shader.set_uniform("projection_matrix", camera.get_projection_matrix()); + + m_planes.models[0].set_color((camera_on_top && type == 1) || (!camera_on_top && type == 2) ? SOLID_PLANE_COLOR : TRANSPARENT_PLANE_COLOR); + m_planes.models[0].render(); + m_planes.models[1].set_color((camera_on_top && type == 2) || (!camera_on_top && type == 1) ? SOLID_PLANE_COLOR : TRANSPARENT_PLANE_COLOR); + m_planes.models[1].render(); +#else + ::glBegin(GL_QUADS); + ::glColor4fv((camera_on_top && type == 1) || (!camera_on_top && type == 2) ? SOLID_PLANE_COLOR.data() : TRANSPARENT_PLANE_COLOR.data()); + ::glVertex3f(min_x, min_y, z1); + ::glVertex3f(max_x, min_y, z1); + ::glVertex3f(max_x, max_y, z1); + ::glVertex3f(min_x, max_y, z1); + glsafe(::glEnd()); + + ::glBegin(GL_QUADS); + ::glColor4fv((camera_on_top && type == 2) || (!camera_on_top && type == 1) ? SOLID_PLANE_COLOR.data() : TRANSPARENT_PLANE_COLOR.data()); + ::glVertex3f(min_x, min_y, z2); + ::glVertex3f(max_x, min_y, z2); + ::glVertex3f(max_x, max_y, z2); + ::glVertex3f(min_x, max_y, z2); + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glDisable(GL_BLEND)); +} + +#ifndef NDEBUG +static bool is_rotation_xy_synchronized(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) +{ + const Eigen::AngleAxisd angle_axis(Geometry::rotation_xyz_diff(rot_xyz_from, rot_xyz_to)); + const Vec3d axis = angle_axis.axis(); + const double angle = angle_axis.angle(); + if (std::abs(angle) < 1e-8) + return true; + assert(std::abs(axis.x()) < 1e-8); + assert(std::abs(axis.y()) < 1e-8); + assert(std::abs(std::abs(axis.z()) - 1.) < 1e-8); + return std::abs(axis.x()) < 1e-8 && std::abs(axis.y()) < 1e-8 && std::abs(std::abs(axis.z()) - 1.) < 1e-8; +} + +static void verify_instances_rotation_synchronized(const Model &model, const GLVolumePtrs &volumes) +{ + for (int idx_object = 0; idx_object < int(model.objects.size()); ++idx_object) { + int idx_volume_first = -1; + for (int i = 0; i < (int)volumes.size(); ++i) { + if (volumes[i]->object_idx() == idx_object) { + idx_volume_first = i; + break; + } + } + assert(idx_volume_first != -1); // object without instances? + if (idx_volume_first == -1) + continue; + const Vec3d &rotation0 = volumes[idx_volume_first]->get_instance_rotation(); + for (int i = idx_volume_first + 1; i < (int)volumes.size(); ++i) + if (volumes[i]->object_idx() == idx_object) { + const Vec3d &rotation = volumes[i]->get_instance_rotation(); + assert(is_rotation_xy_synchronized(rotation, rotation0)); + } + } +} +#endif /* NDEBUG */ + +void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_type) +{ + std::set done; // prevent processing volumes twice + done.insert(m_list.begin(), m_list.end()); + + for (unsigned int i : m_list) { + if (done.size() == m_volumes->size()) + break; + + const GLVolume* volume_i = (*m_volumes)[i]; + if (volume_i->is_wipe_tower) + continue; + + const int object_idx = volume_i->object_idx(); + const int instance_idx = volume_i->instance_idx(); +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& curr_inst_trafo_i = volume_i->get_instance_transformation(); + const Vec3d curr_inst_rotation_i = curr_inst_trafo_i.get_rotation(); + const Vec3d& curr_inst_scaling_factor_i = curr_inst_trafo_i.get_scaling_factor(); + const Vec3d& curr_inst_mirror_i = curr_inst_trafo_i.get_mirror(); + const Vec3d old_inst_rotation_i = m_cache.volumes_data[i].get_instance_transform().get_rotation(); +#else + const Vec3d& rotation = volume_i->get_instance_rotation(); + const Vec3d& scaling_factor = volume_i->get_instance_scaling_factor(); + const Vec3d& mirror = volume_i->get_instance_mirror(); +#endif // ENABLE_WORLD_COORDINATE + + // Process unselected instances. + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (done.size() == m_volumes->size()) + break; + + if (done.find(j) != done.end()) + continue; + + GLVolume* volume_j = (*m_volumes)[j]; + if (volume_j->object_idx() != object_idx || volume_j->instance_idx() == instance_idx) + continue; + +#if ENABLE_WORLD_COORDINATE + const Vec3d old_inst_rotation_j = m_cache.volumes_data[j].get_instance_transform().get_rotation(); + assert(is_rotation_xy_synchronized(old_inst_rotation_i, old_inst_rotation_j)); + const Geometry::Transformation& curr_inst_trafo_j = volume_j->get_instance_transformation(); + const Vec3d curr_inst_rotation_j = curr_inst_trafo_j.get_rotation(); + Vec3d new_inst_offset_j = curr_inst_trafo_j.get_offset(); + Vec3d new_inst_rotation_j = curr_inst_rotation_j; +#else + assert(is_rotation_xy_synchronized(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation())); +#endif // ENABLE_WORLD_COORDINATE + + switch (sync_rotation_type) { + case SyncRotationType::NONE: { + // z only rotation -> synch instance z + // The X,Y rotations should be synchronized from start to end of the rotation. +#if ENABLE_WORLD_COORDINATE + assert(is_rotation_xy_synchronized(curr_inst_rotation_i, curr_inst_rotation_j)); + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) + new_inst_offset_j.z() = curr_inst_trafo_i.get_offset().z(); +#else + assert(is_rotation_xy_synchronized(rotation, volume_j->get_instance_rotation())); + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) + volume_j->set_instance_offset(Z, volume_i->get_instance_offset().z()); +#endif // ENABLE_WORLD_COORDINATE + break; + } + case SyncRotationType::GENERAL: { + // generic rotation -> update instance z with the delta of the rotation. +#if ENABLE_WORLD_COORDINATE + const double z_diff = Geometry::rotation_diff_z(old_inst_rotation_i, old_inst_rotation_j); + new_inst_rotation_j = curr_inst_rotation_i + z_diff * Vec3d::UnitZ(); +#else + const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation()); + volume_j->set_instance_rotation({ rotation.x(), rotation.y(), rotation.z() + z_diff }); +#endif // ENABLE_WORLD_COORDINATE + break; + } +#if ENABLE_WORLD_COORDINATE + case SyncRotationType::FULL: { + // generic rotation -> update instance z with the delta of the rotation. + const Eigen::AngleAxisd angle_axis(Geometry::rotation_xyz_diff(curr_inst_rotation_i, old_inst_rotation_j)); + const Vec3d& axis = angle_axis.axis(); + const double z_diff = (std::abs(axis.x()) > EPSILON || std::abs(axis.y()) > EPSILON) ? + angle_axis.angle() * axis.z() : Geometry::rotation_diff_z(curr_inst_rotation_i, old_inst_rotation_j); + + new_inst_rotation_j = curr_inst_rotation_i + z_diff * Vec3d::UnitZ(); + break; + } +#endif // ENABLE_WORLD_COORDINATE + } + +#if ENABLE_WORLD_COORDINATE + volume_j->set_instance_transformation(Geometry::assemble_transform(new_inst_offset_j, new_inst_rotation_j, + curr_inst_scaling_factor_i, curr_inst_mirror_i)); +#else + volume_j->set_instance_scaling_factor(scaling_factor); + volume_j->set_instance_mirror(mirror); +#endif // ENABLE_WORLD_COORDINATE + + done.insert(j); + } + } + +#ifndef NDEBUG + verify_instances_rotation_synchronized(*m_model, *m_volumes); +#endif /* NDEBUG */ +} + +void Selection::synchronize_unselected_volumes() +{ + for (unsigned int i : m_list) { + const GLVolume* volume = (*m_volumes)[i]; + if (volume->is_wipe_tower) + continue; + + const int object_idx = volume->object_idx(); + const int volume_idx = volume->volume_idx(); +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& trafo = volume->get_volume_transformation(); +#else + const Vec3d& offset = volume->get_volume_offset(); + const Vec3d& rotation = volume->get_volume_rotation(); + const Vec3d& scaling_factor = volume->get_volume_scaling_factor(); + const Vec3d& mirror = volume->get_volume_mirror(); +#endif // ENABLE_WORLD_COORDINATE + + // Process unselected volumes. + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (j == i) + continue; + + GLVolume* v = (*m_volumes)[j]; + if (v->object_idx() != object_idx || v->volume_idx() != volume_idx) + continue; + +#if ENABLE_WORLD_COORDINATE + v->set_volume_transformation(trafo); +#else + v->set_volume_offset(offset); + v->set_volume_rotation(rotation); + v->set_volume_scaling_factor(scaling_factor); + v->set_volume_mirror(mirror); +#endif // ENABLE_WORLD_COORDINATE + } + } +} + +void Selection::ensure_on_bed() +{ + typedef std::map, double> InstancesToZMap; + InstancesToZMap instances_min_z; + + for (size_t i = 0; i < m_volumes->size(); ++i) { + GLVolume* volume = (*m_volumes)[i]; + if (!volume->is_wipe_tower && !volume->is_modifier && + std::find(m_cache.sinking_volumes.begin(), m_cache.sinking_volumes.end(), i) == m_cache.sinking_volumes.end()) { + const double min_z = volume->transformed_convex_hull_bounding_box().min.z(); + std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::iterator it = instances_min_z.find(instance); + if (it == instances_min_z.end()) + it = instances_min_z.insert(InstancesToZMap::value_type(instance, DBL_MAX)).first; + + it->second = std::min(it->second, min_z); + } + } + + for (GLVolume* volume : *m_volumes) { + std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::iterator it = instances_min_z.find(instance); + if (it != instances_min_z.end()) + volume->set_instance_offset(Z, volume->get_instance_offset(Z) - it->second); + } +} + +void Selection::ensure_not_below_bed() +{ + typedef std::map, double> InstancesToZMap; + InstancesToZMap instances_max_z; + + for (size_t i = 0; i < m_volumes->size(); ++i) { + GLVolume* volume = (*m_volumes)[i]; + if (!volume->is_wipe_tower && !volume->is_modifier) { + const double max_z = volume->transformed_convex_hull_bounding_box().max.z(); + const std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::iterator it = instances_max_z.find(instance); + if (it == instances_max_z.end()) + it = instances_max_z.insert({ instance, -DBL_MAX }).first; + + it->second = std::max(it->second, max_z); + } + } + + if (is_any_volume()) { + for (unsigned int i : m_list) { + GLVolume& volume = *(*m_volumes)[i]; + const std::pair instance = std::make_pair(volume.object_idx(), volume.instance_idx()); + InstancesToZMap::const_iterator it = instances_max_z.find(instance); + const double z_shift = SINKING_MIN_Z_THRESHOLD - it->second; + if (it != instances_max_z.end() && z_shift > 0.0) + volume.set_volume_offset(Z, volume.get_volume_offset(Z) + z_shift); + } + } + else { + for (GLVolume* volume : *m_volumes) { + const std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::const_iterator it = instances_max_z.find(instance); + if (it != instances_max_z.end() && it->second < SINKING_MIN_Z_THRESHOLD) + volume->set_instance_offset(Z, volume->get_instance_offset(Z) + SINKING_MIN_Z_THRESHOLD - it->second); + } + } +} + +bool Selection::is_from_fully_selected_instance(unsigned int volume_idx) const +{ + struct SameInstance + { + int obj_idx; + int inst_idx; + GLVolumePtrs& volumes; + + SameInstance(int obj_idx, int inst_idx, GLVolumePtrs& volumes) : obj_idx(obj_idx), inst_idx(inst_idx), volumes(volumes) {} + bool operator () (unsigned int i) { return (volumes[i]->volume_idx() >= 0) && (volumes[i]->object_idx() == obj_idx) && (volumes[i]->instance_idx() == inst_idx); } + }; + + if ((unsigned int)m_volumes->size() <= volume_idx) + return false; + + GLVolume* volume = (*m_volumes)[volume_idx]; + int object_idx = volume->object_idx(); + if ((int)m_model->objects.size() <= object_idx) + return false; + + unsigned int count = (unsigned int)std::count_if(m_list.begin(), m_list.end(), SameInstance(object_idx, volume->instance_idx(), *m_volumes)); + return count == (unsigned int)m_model->objects[object_idx]->volumes.size(); +} + +void Selection::paste_volumes_from_clipboard() +{ +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ + + int dst_obj_idx = get_object_idx(); + if ((dst_obj_idx < 0) || ((int)m_model->objects.size() <= dst_obj_idx)) + return; + + ModelObject* dst_object = m_model->objects[dst_obj_idx]; + + int dst_inst_idx = get_instance_idx(); + if ((dst_inst_idx < 0) || ((int)dst_object->instances.size() <= dst_inst_idx)) + return; + + ModelObject* src_object = m_clipboard.get_object(0); + if (src_object != nullptr) + { + ModelInstance* dst_instance = dst_object->instances[dst_inst_idx]; + BoundingBoxf3 dst_instance_bb = dst_object->instance_bounding_box(dst_inst_idx); +#if ENABLE_WORLD_COORDINATE + Transform3d src_matrix = src_object->instances[0]->get_transformation().get_matrix_no_offset(); + Transform3d dst_matrix = dst_instance->get_transformation().get_matrix_no_offset(); +#else + Transform3d src_matrix = src_object->instances[0]->get_transformation().get_matrix(true); + Transform3d dst_matrix = dst_instance->get_transformation().get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE + bool from_same_object = (src_object->input_file == dst_object->input_file) && src_matrix.isApprox(dst_matrix); + + // used to keep relative position of multivolume selections when pasting from another object + BoundingBoxf3 total_bb; + + ModelVolumePtrs volumes; + for (ModelVolume* src_volume : src_object->volumes) + { + ModelVolume* dst_volume = dst_object->add_volume(*src_volume); + dst_volume->set_new_unique_id(); + if (from_same_object) + { +// // if the volume comes from the same object, apply the offset in world system +// double offset = wxGetApp().plater()->canvas3D()->get_size_proportional_to_max_bed_size(0.05); +// dst_volume->translate(dst_matrix.inverse() * Vec3d(offset, offset, 0.0)); + } + else + { + // if the volume comes from another object, apply the offset as done when adding modifiers + // see ObjectList::load_generic_subobject() + total_bb.merge(dst_volume->mesh().bounding_box().transformed(src_volume->get_matrix())); + } + + volumes.push_back(dst_volume); +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ + } + + // keeps relative position of multivolume selections + if (!from_same_object) + { + for (ModelVolume* v : volumes) + { + v->set_offset((v->get_offset() - total_bb.center()) + dst_matrix.inverse() * (Vec3d(dst_instance_bb.max(0), dst_instance_bb.min(1), dst_instance_bb.min(2)) + 0.5 * total_bb.size() - dst_instance->get_transformation().get_offset())); + } + } + + wxGetApp().obj_list()->paste_volumes_into_list(dst_obj_idx, volumes); + } + +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ +} + +void Selection::paste_objects_from_clipboard() +{ +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ + + std::vector object_idxs; + const ModelObjectPtrs& src_objects = m_clipboard.get_objects(); + for (const ModelObject* src_object : src_objects) + { + ModelObject* dst_object = m_model->add_object(*src_object); + double offset = wxGetApp().plater()->canvas3D()->get_size_proportional_to_max_bed_size(0.05); + Vec3d displacement(offset, offset, 0.0); + for (ModelInstance* inst : dst_object->instances) + { + inst->set_offset(inst->get_offset() + displacement); + } + + object_idxs.push_back(m_model->objects.size() - 1); +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ + } + + wxGetApp().obj_list()->paste_objects_into_list(object_idxs); + +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ +} + +#if ENABLE_WORLD_COORDINATE +void Selection::transform_volume_relative(GLVolume& volume, const VolumeCache& volume_data, TransformationType transformation_type, + const Transform3d& transform) +{ + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + const Geometry::Transformation& volume_trafo = volume_data.get_volume_transform(); + if (transformation_type.world()) { + const Transform3d inst_matrix_no_offset = inst_trafo.get_matrix_no_offset(); + const Transform3d new_volume_matrix = inst_matrix_no_offset.inverse() * transform * inst_matrix_no_offset; + volume.set_volume_transformation(volume_trafo.get_offset_matrix() * new_volume_matrix * volume_trafo.get_matrix_no_offset()); + } + else if (transformation_type.instance()) + volume.set_volume_transformation(volume_trafo.get_offset_matrix() * transform * volume_trafo.get_matrix_no_offset()); + else if (transformation_type.local()) { + const Geometry::Transformation trafo(transform); + volume.set_volume_transformation(trafo.get_offset_matrix() * volume_trafo.get_matrix() * trafo.get_matrix_no_offset()); + } + else + assert(false); +} +#endif // ENABLE_WORLD_COORDINATE + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index c4720a3081..34b88f1606 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -331,6 +331,7 @@ public: bool is_single_volume_or_modifier() const { return is_single_volume() || is_single_modifier(); } #endif // ENABLE_WORLD_COORDINATE bool is_single_volume_instance() const { return is_single_full_instance() && m_list.size() == 1; } + bool is_single_text() const; bool contains_volume(unsigned int volume_idx) const { return m_list.find(volume_idx) != m_list.end(); } // returns true if the selection contains all the given indices @@ -364,6 +365,7 @@ public: const IndicesList& get_volume_idxs() const { return m_list; } const GLVolume* get_volume(unsigned int volume_idx) const; const GLVolume* get_first_volume() const { return get_volume(*m_list.begin()); } + GLVolume* get_volume(unsigned int volume_idx); const ObjectIdxsToInstanceIdxsMap& get_content() const { return m_cache.content; } diff --git a/src/slic3r/Utils/EmbossStyleManager.cpp b/src/slic3r/Utils/EmbossStyleManager.cpp new file mode 100644 index 0000000000..256b1160b0 --- /dev/null +++ b/src/slic3r/Utils/EmbossStyleManager.cpp @@ -0,0 +1,487 @@ +#include "EmbossStyleManager.hpp" +#include // Imgui texture +#include // ImTextCharFromUtf8 +#include "WxFontUtils.hpp" +#include "libslic3r/Utils.hpp" // ScopeGuard + +#include "slic3r/GUI/3DScene.hpp" // ::glsafe +#include "slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp" +#include "slic3r/GUI/ImGuiWrapper.hpp" // check of font ranges + +#include "slic3r/Utils/EmbossStylesSerializable.hpp" + +using namespace Slic3r; +using namespace Slic3r::Emboss; +using namespace Slic3r::GUI::Emboss; + +StyleManager::StyleManager(const ImWchar *language_glyph_range) + : m_imgui_init_glyph_range(language_glyph_range) + , m_exist_style_images(false) + , m_temp_style_images(nullptr) + , m_app_config(nullptr) + , m_last_style_index(std::numeric_limits::max()) +{} + +StyleManager::~StyleManager() { + clear_imgui_font(); + free_style_images(); +} + +void StyleManager::init(AppConfig *app_config, const EmbossStyles &default_styles) +{ + m_app_config = app_config; + EmbossStyles styles = (app_config != nullptr) ? + EmbossStylesSerializable::load_styles(*app_config) : + default_styles; + if (styles.empty()) styles = default_styles; + for (EmbossStyle &style : styles) { + make_unique_name(style.name); + m_style_items.push_back({style}); + } + + std::optional activ_index_opt = (app_config != nullptr) ? + EmbossStylesSerializable::load_style_index(*app_config) : + std::optional{}; + + size_t activ_index = 0; + if (activ_index_opt.has_value()) activ_index = *activ_index_opt; + if (activ_index >= m_style_items.size()) activ_index = 0; + + // find valid font item + if (!load_style(activ_index)) { + m_style_items.erase(m_style_items.begin() + activ_index); + activ_index = 0; + while (m_style_items.empty() || !load_style(activ_index)) + m_style_items.erase(m_style_items.begin()); + // no one style from config is loadable + if (m_style_items.empty()) { + // set up default font list + for (EmbossStyle style : default_styles) { + make_unique_name(style.name); + m_style_items.push_back({std::move(style)}); + } + // try to load first default font + [[maybe_unused]] bool loaded = load_style(activ_index); + assert(loaded); + } + } +} + +bool StyleManager::store_styles_to_app_config(bool use_modification, + bool store_activ_index) +{ + assert(m_app_config != nullptr); + if (m_app_config == nullptr) return false; + if (use_modification) { + if (exist_stored_style()) { + // update stored item + m_style_items[m_style_cache.style_index].style = m_style_cache.style; + } else { + // add new into stored list + EmbossStyle &style = m_style_cache.style; + make_unique_name(style.name); + m_style_cache.truncated_name.clear(); + m_style_cache.style_index = m_style_items.size(); + m_style_items.push_back({style}); + } + m_style_cache.stored_wx_font = m_style_cache.wx_font; + } + + if (store_activ_index) + { + size_t style_index = exist_stored_style() ? + m_style_cache.style_index : + m_last_style_index; + EmbossStylesSerializable::store_style_index(*m_app_config, style_index); + } + + EmbossStyles styles; + styles.reserve(m_style_items.size()); + for (const Item &item : m_style_items) styles.push_back(item.style); + EmbossStylesSerializable::store_styles(*m_app_config, styles); + return true; +} + +void StyleManager::add_style(const std::string &name) { + EmbossStyle& style = m_style_cache.style; + style.name = name; + make_unique_name(style.name); + m_style_cache.style_index = m_style_items.size(); + m_style_cache.stored_wx_font = m_style_cache.wx_font; + m_style_cache.truncated_name.clear(); + m_style_items.push_back({style}); +} + +void StyleManager::swap(size_t i1, size_t i2) { + if (i1 >= m_style_items.size() || + i2 >= m_style_items.size()) return; + std::swap(m_style_items[i1], m_style_items[i2]); + // fix selected index + if (!exist_stored_style()) return; + if (m_style_cache.style_index == i1) { + m_style_cache.style_index = i2; + } else if (m_style_cache.style_index == i2) { + m_style_cache.style_index = i1; + } +} + +void StyleManager::discard_style_changes() { + if (exist_stored_style()) { + if (load_style(m_style_cache.style_index)) + return; // correct reload style + } else { + if(load_style(m_last_style_index)) + return; // correct load last used style + } + + // try to save situation by load some font + load_first_valid_font(); +} + +void StyleManager::erase(size_t index) { + if (index >= m_style_items.size()) return; + + // fix selected index + if (exist_stored_style()) { + size_t &i = m_style_cache.style_index; + if (index < i) --i; + else if (index == i) i = std::numeric_limits::max(); + } + + m_style_items.erase(m_style_items.begin() + index); +} + +void StyleManager::rename(const std::string& name) { + m_style_cache.style.name = name; + m_style_cache.truncated_name.clear(); + if (exist_stored_style()) { + Item &it = m_style_items[m_style_cache.style_index]; + it.style.name = name; + it.truncated_name.clear(); + } +} + +bool StyleManager::load_style(size_t style_index) +{ + if (style_index >= m_style_items.size()) return false; + if (!load_style(m_style_items[style_index].style)) return false; + m_style_cache.style_index = style_index; + m_style_cache.stored_wx_font = m_style_cache.wx_font; // copy + m_last_style_index = style_index; + return true; +} + +bool StyleManager::load_style(const EmbossStyle &style) { + if (style.type == EmbossStyle::Type::file_path) { + std::unique_ptr font_ptr = + create_font_file(style.path.c_str()); + if (font_ptr == nullptr) return false; + m_style_cache.wx_font = {}; + m_style_cache.font_file = + FontFileWithCache(std::move(font_ptr)); + m_style_cache.style = style; // copy + m_style_cache.style_index = std::numeric_limits::max(); + m_style_cache.stored_wx_font = {}; + return true; + } + if (style.type != WxFontUtils::get_actual_type()) return false; + std::optional wx_font_opt = WxFontUtils::load_wxFont(style.path); + if (!wx_font_opt.has_value()) return false; + return load_style(style, *wx_font_opt); +} + +bool StyleManager::load_style(const EmbossStyle &style, const wxFont &font) +{ + if (!set_wx_font(font)) return false; + m_style_cache.style = style; // copy + m_style_cache.style_index = std::numeric_limits::max(); + m_style_cache.stored_wx_font = {}; + m_style_cache.truncated_name.clear(); + return true; +} + +bool StyleManager::is_activ_font() { return m_style_cache.font_file.has_value(); } + +bool StyleManager::load_first_valid_font() { + while (!m_style_items.empty()) { + if (load_style(0)) return true; + // can't load so erase it from list + m_style_items.erase(m_style_items.begin()); + } + return false; +} + +const EmbossStyle* StyleManager::get_stored_style() const +{ + if (m_style_cache.style_index >= m_style_items.size()) return nullptr; + return &m_style_items[m_style_cache.style_index].style; +} + +void StyleManager::clear_glyphs_cache() +{ + FontFileWithCache &ff = m_style_cache.font_file; + if (!ff.has_value()) return; + ff.cache = std::make_shared(); +} + +void StyleManager::clear_imgui_font() { m_style_cache.atlas.Clear(); } + +ImFont *StyleManager::get_imgui_font() +{ + if (!is_activ_font()) return nullptr; + + ImVector &fonts = m_style_cache.atlas.Fonts; + if (fonts.empty()) return nullptr; + + // check correct index + int f_size = fonts.size(); + assert(f_size == 1); + if (f_size != 1) return nullptr; + ImFont *font = fonts.front(); + if (font == nullptr) return nullptr; + return font; +} + +const std::vector &StyleManager::get_styles() const{ return m_style_items; } + +ImFont* StyleManager::extend_imgui_font_range(size_t index, const std::string& text) +{ + // TODO: start using merge mode + // ImFontConfig::MergeMode = true; + return create_imgui_font(text); +} + +void StyleManager::make_unique_name(std::string &name) +{ + auto is_unique = [&](const std::string &name) -> bool { + for (const Item &it : m_style_items) + if (it.style.name == name) return false; + return true; + }; + + // Style name can't be empty so default name is set + if (name.empty()) name = "Text style"; + + // When name is already unique, nothing need to be changed + if (is_unique(name)) return; + + // when there is previous version of style name only find number + const char *prefix = " ("; + const char suffix = ')'; + auto pos = name.find_last_of(prefix); + if (name.c_str()[name.size() - 1] == suffix && + pos != std::string::npos) { + // short name by ord number + name = name.substr(0, pos); + } + + int order = 1; // start with value 2 to represents same font name + std::string new_name; + do { + new_name = name + prefix + std::to_string(++order) + suffix; + } while (!is_unique(new_name)); + name = new_name; +} + +void StyleManager::init_trunc_names(float max_width) { + for (auto &s : m_style_items) + if (s.truncated_name.empty()) { + std::string name = s.style.name; + ImGuiWrapper::escape_double_hash(name); + s.truncated_name = ImGuiWrapper::trunc(name, max_width); + } +} + +#include "slic3r/GUI/Jobs/CreateFontStyleImagesJob.hpp" + +// for access to worker +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" + +void StyleManager::init_style_images(const Vec2i &max_size, + const std::string &text) +{ + // check already initialized + if (m_exist_style_images) return; + + // check is initializing + if (m_temp_style_images != nullptr) { + // is initialization finished + if (!m_temp_style_images->styles.empty()) { + assert(m_temp_style_images->images.size() == + m_temp_style_images->styles.size()); + // copy images into styles + for (StyleManager::StyleImage &image : m_temp_style_images->images){ + size_t index = &image - &m_temp_style_images->images.front(); + StyleImagesData::Item &style = m_temp_style_images->styles[index]; + + // find style in font list and copy to it + for (auto &it : m_style_items) { + if (it.style.name != style.text || + !(it.style.prop == style.prop)) + continue; + it.image = image; + break; + } + } + m_temp_style_images = nullptr; + m_exist_style_images = true; + return; + } + // in process of initialization inside of job + return; + } + + // create job for init images + m_temp_style_images = std::make_shared(); + StyleImagesData::Items styles; + styles.reserve(m_style_items.size()); + for (const Item &item : m_style_items) { + const EmbossStyle &style = item.style; + std::optional wx_font_opt = WxFontUtils::load_wxFont(style.path); + if (!wx_font_opt.has_value()) continue; + std::unique_ptr font_file = + WxFontUtils::create_font_file(*wx_font_opt); + if (font_file == nullptr) continue; + styles.push_back({ + FontFileWithCache(std::move(font_file)), + style.name, + style.prop + }); + } + auto &worker = wxGetApp().plater()->get_ui_job_worker(); + StyleImagesData data{std::move(styles), max_size, text, m_temp_style_images}; + queue_job(worker, std::make_unique(std::move(data))); +} + +void StyleManager::free_style_images() { + if (!m_exist_style_images) return; + GLuint tex_id = 0; + for (Item &it : m_style_items) { + if (tex_id == 0 && it.image.has_value()) + tex_id = (GLuint)(intptr_t) it.image->texture_id; + it.image.reset(); + } + if (tex_id != 0) + glsafe(::glDeleteTextures(1, &tex_id)); + m_exist_style_images = false; +} + +float StyleManager::min_imgui_font_size = 18.f; +float StyleManager::max_imgui_font_size = 60.f; +float StyleManager::get_imgui_font_size(const FontProp &prop, + const FontFile &file) +{ + const auto &cn = prop.collection_number; + unsigned int font_index = (cn.has_value()) ? *cn : 0; + const auto &font_info = file.infos[font_index]; + // coeficient for convert line height to font size + float c1 = (font_info.ascent - font_info.descent + font_info.linegap) / + (float) font_info.unit_per_em; + + // The point size is defined as 1/72 of the Anglo-Saxon inch (25.4 mm): + // It is approximately 0.0139 inch or 352.8 um. + return c1 * std::abs(prop.size_in_mm) / 0.3528f; +} + +ImFont *StyleManager::create_imgui_font(const std::string &text) +{ + // inspiration inside of ImGuiWrapper::init_font + auto& ff = m_style_cache.font_file; + if (!ff.has_value()) return nullptr; + const FontFile &font_file = *ff.font_file; + + ImFontGlyphRangesBuilder builder; + builder.AddRanges(m_imgui_init_glyph_range); + if (!text.empty()) + builder.AddText(text.c_str()); + + ImVector &ranges = m_style_cache.ranges; + ranges.clear(); + builder.BuildRanges(&ranges); + + m_style_cache.atlas.Flags |= ImFontAtlasFlags_NoMouseCursors | + ImFontAtlasFlags_NoPowerOfTwoHeight; + + const FontProp &font_prop = m_style_cache.style.prop; + float font_size = get_imgui_font_size(font_prop, font_file); + if (font_size < min_imgui_font_size) + font_size = min_imgui_font_size; + if (font_size > max_imgui_font_size) + font_size = max_imgui_font_size; + + ImFontConfig font_config; + // TODO: start using merge mode + //font_config.MergeMode = true; + + const auto &cn = font_prop.collection_number; + unsigned int font_index = (cn.has_value()) ? *cn : 0; + const auto &font_info = font_file.infos[font_index]; + if (font_prop.char_gap.has_value()) { + float coef = font_size / (double) font_info.unit_per_em; + font_config.GlyphExtraSpacing.x = coef * (*font_prop.char_gap); + } + if (font_prop.line_gap.has_value()) { + float coef = font_size / (double) font_info.unit_per_em; + font_config.GlyphExtraSpacing.y = coef * (*font_prop.line_gap); + } + + font_config.FontDataOwnedByAtlas = false; + + const std::vector &buffer = *font_file.data; + ImFont * font = m_style_cache.atlas.AddFontFromMemoryTTF( + (void *) buffer.data(), buffer.size(), font_size, &font_config, m_style_cache.ranges.Data); + + unsigned char *pixels; + int width, height; + m_style_cache.atlas.GetTexDataAsRGBA32(&pixels, &width, &height); + + // Upload texture to graphics system + GLint last_texture; + glsafe(::glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture)); + ScopeGuard sg([last_texture]() { + glsafe(::glBindTexture(GL_TEXTURE_2D, last_texture)); + }); + + GLuint font_texture; + glsafe(::glGenTextures(1, &font_texture)); + glsafe(::glBindTexture(GL_TEXTURE_2D, font_texture)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + glsafe(::glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)); + if (OpenGLManager::are_compressed_textures_supported()) + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels)); + else + glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels)); + + // Store our identifier + m_style_cache.atlas.TexID = (ImTextureID) (intptr_t) font_texture; + assert(!m_style_cache.atlas.Fonts.empty()); + if (m_style_cache.atlas.Fonts.empty()) return nullptr; + assert(font == m_style_cache.atlas.Fonts.back()); + if (!font->IsLoaded()) return nullptr; + assert(font->IsLoaded()); + return font; +} + +bool StyleManager::set_wx_font(const wxFont &wx_font) { + std::unique_ptr font_file = + WxFontUtils::create_font_file(wx_font); + return set_wx_font(wx_font, std::move(font_file)); +} + +bool StyleManager::set_wx_font(const wxFont &wx_font, std::unique_ptr font_file) +{ + if (font_file == nullptr) return false; + m_style_cache.wx_font = wx_font; // copy + m_style_cache.font_file = + FontFileWithCache(std::move(font_file)); + + EmbossStyle &style = m_style_cache.style; + style.type = WxFontUtils::get_actual_type(); + // update string path + style.path = WxFontUtils::store_wxFont(wx_font); + WxFontUtils::update_property(style.prop, wx_font); + clear_imgui_font(); + return true; +} diff --git a/src/slic3r/Utils/EmbossStyleManager.hpp b/src/slic3r/Utils/EmbossStyleManager.hpp new file mode 100644 index 0000000000..502ba84bd5 --- /dev/null +++ b/src/slic3r/Utils/EmbossStyleManager.hpp @@ -0,0 +1,291 @@ +#ifndef slic3r_EmbossStyleManager_hpp_ +#define slic3r_EmbossStyleManager_hpp_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Slic3r::GUI::Emboss { +/// +/// Manage Emboss text styles +/// Cache actual state of style +/// + imgui font +/// + wx font +/// +class StyleManager +{ + friend class CreateFontStyleImagesJob; // access to StyleImagesData + +public: + StyleManager(const ImWchar *language_glyph_range); + + /// + /// Release imgui font and style images from GPU + /// + ~StyleManager(); + + /// + /// Load font style list from config + /// Also select actual activ font + /// + /// Application configuration loaded from file "PrusaSlicer.ini" + /// + cfg is stored to privat variable + /// Used when list is not loadable from config + void init(AppConfig *app_config, const EmbossStyles &default_styles); + + /// + /// Write font list into AppConfig + /// + /// Configuration + /// When true cache state will be used for store + /// When true store activ index into configuration + /// True on succes otherwise False. + bool store_styles_to_app_config(bool use_modification = true, bool store_activ_index = true); + + /// + /// Append actual style to style list + /// + /// New name for style + void add_style(const std::string& name); + + /// + /// Change order of style item in m_style_items. + /// Fix selected font index when (i1 || i2) == m_font_selected + /// + /// First index to m_style_items + /// Second index to m_style_items + void swap(size_t i1, size_t i2); + + /// + /// Discard changes in activ style + /// When no activ style use last used OR first loadable + /// + void discard_style_changes(); + + /// + /// Remove style from m_style_items. + /// Fix selected font index when index is under m_font_selected + /// + /// Index of style to be removed + void erase(size_t index); + + /// + /// Rename actual selected font item + /// + /// New name + void rename(const std::string &name); + + /// + /// Change active font + /// When font not loaded roll back activ font + /// + /// New font index(from m_style_items range) + /// True on succes. False on fail load font + bool load_style(size_t font_index); + // load font style not stored in list + bool load_style(const EmbossStyle &style); + // fastering load font on index by wxFont, ignore type and descriptor + bool load_style(const EmbossStyle &style, const wxFont &font); + + // clear actual selected glyphs cache + void clear_glyphs_cache(); + + // remove cached imgui font for actual selected font + void clear_imgui_font(); + + // getters for private data + const EmbossStyle *get_stored_style() const; + + const EmbossStyle &get_style() const { return m_style_cache.style; } + EmbossStyle &get_style() { return m_style_cache.style; } + size_t get_style_index() const { return m_style_cache.style_index; } + std::string &get_truncated_name() { return m_style_cache.truncated_name; } + const ImFontAtlas &get_atlas() const { return m_style_cache.atlas; } + const FontProp &get_font_prop() const { return get_style().prop; } + FontProp &get_font_prop() { return get_style().prop; } + const std::optional &get_wx_font() const { return m_style_cache.wx_font; } + const std::optional &get_stored_wx_font() const { return m_style_cache.stored_wx_font; } + Slic3r::Emboss::FontFileWithCache &get_font_file_with_cache() { return m_style_cache.font_file; } + bool has_collections() const { return m_style_cache.font_file.font_file != nullptr && + m_style_cache.font_file.font_file->infos.size() > 1; } + + // True when activ style has same name as some of stored style + bool exist_stored_style() const { return m_style_cache.style_index != std::numeric_limits::max(); } + + /// + /// Setter on wx_font when changed + /// + /// new wx font + /// True on success set otherwise FALSE + bool set_wx_font(const wxFont &wx_font); + + /// + /// Faster way of set wx_font when font file is known(do not load font file twice) + /// When you not sure that wx_font is made by font_file use only set_wx_font(wx_font) + /// + /// Must be source of font file + /// font file created by WxFontUtils::create_font_file(wx_font) + /// True on success otherwise false + bool set_wx_font(const wxFont &wx_font, std::unique_ptr font_file); + + // Getter on acitve font pointer for imgui + // Initialize imgui font(generate texture) when doesn't exist yet. + // Extend font atlas when not in glyph range + ImFont *get_imgui_font(); + // initialize font range by unique symbols in text + ImFont *create_imgui_font(const std::string& text); + + // init truncated names of styles + void init_trunc_names(float max_width); + + /// + /// Initialization texture with rendered font style + /// + /// Maximal width and height of one style texture + /// Text to render by style + void init_style_images(const Vec2i& max_size, const std::string &text); + void free_style_images(); + + struct Item; + // access to all managed font styles + const std::vector &get_styles() const; + + /// + /// Describe image in GPU to show settings of style + /// + struct StyleImage + { + void* texture_id = 0; // GLuint + BoundingBox bounding_box; + ImVec2 tex_size, uv0, uv1; + Point offset = Point(0, 0); + StyleImage() = default; + }; + + /// + /// All connected with one style + /// keep temporary data and caches for style + /// + struct Item + { + // define font, style and other property of text + EmbossStyle style; + + // cache for view font name with maximal width in imgui + std::string truncated_name; + + // visualization of style + std::optional image; + }; + + // check if exist selected font style in manager + bool is_activ_font(); + + // Limits for imgui loaded font size + // Value out of limits is crop + static float min_imgui_font_size; + static float max_imgui_font_size; + static float get_imgui_font_size(const FontProp &prop, const Slic3r::Emboss::FontFile &file); + +private: + // erase font when not possible to load + // used at initialize phaze - fonts could be modified in appConfig file by user + bool load_first_valid_font(); + + /// + /// Cache data from style to reduce amount of: + /// 1) loading font from file + /// 2) Create atlas of symbols for imgui + /// 3) Keep loaded(and modified by style) glyphs from font + /// + struct StyleCache + { + // share font file data with emboss job thread + Slic3r::Emboss::FontFileWithCache font_file = {}; + + // must live same as imgui_font inside of atlas + ImVector ranges = {}; + + // Keep only actual style in atlas + ImFontAtlas atlas = {}; + + // wx widget font + std::optional wx_font = {}; + + // cache for view font name with maximal width in imgui + std::string truncated_name; + + // actual used font item + EmbossStyle style = {}; + + // cache for stored wx font to not create every frame + std::optional stored_wx_font; + + // index into m_style_items + size_t style_index = std::numeric_limits::max(); + + } m_style_cache; + + // extend actual imgui font when exist unknown char in text + // NOTE: imgui_font has to be unused + // return true when extend range otherwise FALSE + ImFont *extend_imgui_font_range(size_t font_index, const std::string &text); + + void make_unique_name(std::string &name); + + // Privat member + std::vector m_style_items; + AppConfig *m_app_config; + size_t m_last_style_index; + + /// + /// Keep data needed to create Font Style Images in Job + /// + struct StyleImagesData + { + struct Item + { + Slic3r::Emboss::FontFileWithCache font; + std::string text; + FontProp prop; + }; + using Items = std::vector; + + // Keep styles to render + Items styles; + // Maximal width and height in pixels of image + Vec2i max_size; + // Text to render + std::string text; + + /// + /// Result of job + /// + struct StyleImages + { + // vector of inputs + StyleImagesData::Items styles; + // job output + std::vector images; + }; + + // place to store result in main thread in Finalize + std::shared_ptr result; + }; + std::shared_ptr m_temp_style_images; + bool m_exist_style_images; + + // store all font GLImages + //ImFontAtlas m_imgui_font_atlas; + const ImWchar *m_imgui_init_glyph_range; +}; + +} // namespace Slic3r + +#endif // slic3r_EmbossStyleManager_hpp_ diff --git a/src/slic3r/Utils/EmbossStylesSerializable.cpp b/src/slic3r/Utils/EmbossStylesSerializable.cpp new file mode 100644 index 0000000000..483f147b11 --- /dev/null +++ b/src/slic3r/Utils/EmbossStylesSerializable.cpp @@ -0,0 +1,202 @@ +#include "EmbossStylesSerializable.hpp" + +#include +#include "WxFontUtils.hpp" + +using namespace Slic3r; +using namespace Slic3r::GUI; + +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_NAME = "name"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_DESCRIPTOR = "descriptor"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_LINE_HEIGHT = "line_height"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_DEPTH = "depth"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_USE_SURFACE = "use_surface"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_BOLDNESS = "boldness"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_SKEW = "skew"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_DISTANCE = "distance"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_ANGLE = "angle"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_COLLECTION = "collection"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_CHAR_GAP = "char_gap"; +const std::string EmbossStylesSerializable::APP_CONFIG_FONT_LINE_GAP = "line_gap"; + +const std::string EmbossStylesSerializable::APP_CONFIG_ACTIVE_FONT = "activ_font"; + +std::string EmbossStylesSerializable::create_section_name(unsigned index) +{ + return AppConfig::SECTION_EMBOSS_STYLE + ':' + std::to_string(index); +} + +// check only existence of flag +bool EmbossStylesSerializable::read(const std::map& section, const std::string& key, bool& value){ + auto item = section.find(key); + if (item == section.end()) return false; + + value = true; + return true; +} + +#include "fast_float/fast_float.h" +bool EmbossStylesSerializable::read(const std::map& section, const std::string& key, float& value){ + auto item = section.find(key); + if (item == section.end()) return false; + const std::string &data = item->second; + if (data.empty()) return false; + float value_; + fast_float::from_chars(data.c_str(), data.c_str() + data.length(), value_); + // read only non zero value + if (fabs(value_) <= std::numeric_limits::epsilon()) return false; + + value = value_; + return true; +} + +bool EmbossStylesSerializable::read(const std::map& section, const std::string& key, std::optional& value){ + auto item = section.find(key); + if (item == section.end()) return false; + const std::string &data = item->second; + if (data.empty()) return false; + int value_ = std::atoi(data.c_str()); + if (value_ == 0) return false; + + value = value_; + return true; +} + +bool EmbossStylesSerializable::read(const std::map& section, const std::string& key, std::optional& value){ + auto item = section.find(key); + if (item == section.end()) return false; + const std::string &data = item->second; + if (data.empty()) return false; + int value_ = std::atoi(data.c_str()); + if (value_ <= 0) return false; + + value = static_cast(value_); + return true; +} + +bool EmbossStylesSerializable::read(const std::map& section, const std::string& key, std::optional& value){ + auto item = section.find(key); + if (item == section.end()) return false; + const std::string &data = item->second; + if (data.empty()) return false; + float value_; + fast_float::from_chars(data.c_str(), data.c_str() + data.length(), value_); + // read only non zero value + if (fabs(value_) <= std::numeric_limits::epsilon()) return false; + + value = value_; + return true; +} + +std::optional EmbossStylesSerializable::load_style( + const std::map &app_cfg_section) +{ + auto path_it = app_cfg_section.find(APP_CONFIG_FONT_DESCRIPTOR); + if (path_it == app_cfg_section.end()) return {}; + const std::string &path = path_it->second; + + auto name_it = app_cfg_section.find(APP_CONFIG_FONT_NAME); + const std::string default_name = "font_name"; + const std::string &name = + (name_it == app_cfg_section.end()) ? + default_name : name_it->second; + + FontProp fp; + read(app_cfg_section, APP_CONFIG_FONT_LINE_HEIGHT, fp.size_in_mm); + read(app_cfg_section, APP_CONFIG_FONT_DEPTH, fp.emboss); + read(app_cfg_section, APP_CONFIG_FONT_USE_SURFACE, fp.use_surface); + read(app_cfg_section, APP_CONFIG_FONT_BOLDNESS, fp.boldness); + read(app_cfg_section, APP_CONFIG_FONT_SKEW, fp.skew); + read(app_cfg_section, APP_CONFIG_FONT_DISTANCE, fp.distance); + read(app_cfg_section, APP_CONFIG_FONT_ANGLE, fp.angle); + read(app_cfg_section, APP_CONFIG_FONT_COLLECTION, fp.collection_number); + read(app_cfg_section, APP_CONFIG_FONT_CHAR_GAP, fp.char_gap); + read(app_cfg_section, APP_CONFIG_FONT_LINE_GAP, fp.line_gap); + + EmbossStyle::Type type = WxFontUtils::get_actual_type(); + return EmbossStyle{ name, path, type, fp }; +} + +void EmbossStylesSerializable::store_style(AppConfig & cfg, + const EmbossStyle &fi, + unsigned index) +{ + std::string section_name = create_section_name(index); + cfg.clear_section(section_name); + cfg.set(section_name, APP_CONFIG_FONT_NAME, fi.name); + cfg.set(section_name, APP_CONFIG_FONT_DESCRIPTOR, fi.path); + const FontProp &fp = fi.prop; + cfg.set(section_name, APP_CONFIG_FONT_LINE_HEIGHT, std::to_string(fp.size_in_mm)); + cfg.set(section_name, APP_CONFIG_FONT_DEPTH, std::to_string(fp.emboss)); + if (fp.use_surface) + cfg.set(section_name, APP_CONFIG_FONT_USE_SURFACE, "true"); + if (fp.boldness.has_value()) + cfg.set(section_name, APP_CONFIG_FONT_BOLDNESS, std::to_string(*fp.boldness)); + if (fp.skew.has_value()) + cfg.set(section_name, APP_CONFIG_FONT_SKEW, std::to_string(*fp.skew)); + if (fp.distance.has_value()) + cfg.set(section_name, APP_CONFIG_FONT_DISTANCE, std::to_string(*fp.distance)); + if (fp.angle.has_value()) + cfg.set(section_name, APP_CONFIG_FONT_ANGLE, std::to_string(*fp.angle)); + if (fp.collection_number.has_value()) + cfg.set(section_name, APP_CONFIG_FONT_COLLECTION, std::to_string(*fp.collection_number)); + if (fp.char_gap.has_value()) + cfg.set(section_name, APP_CONFIG_FONT_CHAR_GAP, std::to_string(*fp.char_gap)); + if (fp.line_gap.has_value()) + cfg.set(section_name, APP_CONFIG_FONT_LINE_GAP, std::to_string(*fp.line_gap)); +} + +void EmbossStylesSerializable::store_style_index(AppConfig &cfg, unsigned index) { + // store actual font index + cfg.clear_section(AppConfig::SECTION_EMBOSS_STYLE); + // activ font first index is +1 to correspond with section name + std::string activ_font = std::to_string(index); + cfg.set(AppConfig::SECTION_EMBOSS_STYLE, APP_CONFIG_ACTIVE_FONT, activ_font); +} + +std::optional EmbossStylesSerializable::load_style_index(const AppConfig &cfg) +{ + if (!cfg.has_section(AppConfig::SECTION_EMBOSS_STYLE)) return {}; + + auto section = cfg.get_section(AppConfig::SECTION_EMBOSS_STYLE); + auto it = section.find(APP_CONFIG_ACTIVE_FONT); + if (it == section.end()) return {}; + + size_t active_font = static_cast(std::atoi(it->second.c_str())); + // order in config starts with number 1 + return active_font - 1; +} + +EmbossStyles EmbossStylesSerializable::load_styles(const AppConfig &cfg) +{ + EmbossStyles result; + // human readable index inside of config starts from 1 !! + unsigned index = 1; + std::string section_name = create_section_name(index); + while (cfg.has_section(section_name)) { + std::optional style_opt = load_style(cfg.get_section(section_name)); + if (style_opt.has_value()) result.emplace_back(*style_opt); + section_name = create_section_name(++index); + } + return result; +} + +void EmbossStylesSerializable::store_styles(AppConfig &cfg, const EmbossStyles& styles) +{ + // store styles + unsigned index = 1; + for (const EmbossStyle &style : styles) { + // skip file paths + fonts from other OS(loaded from .3mf) + assert(style.type == WxFontUtils::get_actual_type()); + // if (style_opt.type != WxFontUtils::get_actual_type()) continue; + store_style(cfg, style, index++); + } + + // remove rest of font sections (after deletation) + std::string section_name = create_section_name(index); + while (cfg.has_section(section_name)) { + cfg.clear_section(section_name); + section_name = create_section_name(++index); + } + cfg.save(); +} \ No newline at end of file diff --git a/src/slic3r/Utils/EmbossStylesSerializable.hpp b/src/slic3r/Utils/EmbossStylesSerializable.hpp new file mode 100644 index 0000000000..da87af8203 --- /dev/null +++ b/src/slic3r/Utils/EmbossStylesSerializable.hpp @@ -0,0 +1,58 @@ +#ifndef slic3r_EmbossStylesSerializable_hpp_ +#define slic3r_EmbossStylesSerializable_hpp_ + +#include +#include +#include +#include // EmbossStyles+EmbossStyle + +namespace Slic3r { +class AppConfig; +} + +namespace Slic3r::GUI { + +/// +/// For store/load font list to/from AppConfig +/// +class EmbossStylesSerializable +{ + static const std::string APP_CONFIG_FONT_NAME; + static const std::string APP_CONFIG_FONT_DESCRIPTOR; + static const std::string APP_CONFIG_FONT_LINE_HEIGHT; + static const std::string APP_CONFIG_FONT_DEPTH; + static const std::string APP_CONFIG_FONT_USE_SURFACE; + static const std::string APP_CONFIG_FONT_BOLDNESS; + static const std::string APP_CONFIG_FONT_SKEW; + static const std::string APP_CONFIG_FONT_DISTANCE; + static const std::string APP_CONFIG_FONT_ANGLE; + static const std::string APP_CONFIG_FONT_COLLECTION; + static const std::string APP_CONFIG_FONT_CHAR_GAP; + static const std::string APP_CONFIG_FONT_LINE_GAP; + + static const std::string APP_CONFIG_ACTIVE_FONT; +public: + EmbossStylesSerializable() = delete; + + static void store_style_index(AppConfig &cfg, unsigned index); + static std::optional load_style_index(const AppConfig &cfg); + + static EmbossStyles load_styles(const AppConfig &cfg); + static void store_styles(AppConfig &cfg, const EmbossStyles& styles); + +private: + static std::string create_section_name(unsigned index); + static std::optional load_style(const std::map &app_cfg_section); + static void store_style(AppConfig &cfg, const EmbossStyle &style, unsigned index); + + // TODO: move to app config like read from section + static bool read(const std::map& section, const std::string& key, bool& value); + static bool read(const std::map& section, const std::string& key, float& value); + static bool read(const std::map& section, const std::string& key, std::optional& value); + static bool read(const std::map& section, const std::string& key, std::optional& value); + static bool read(const std::map& section, const std::string& key, std::optional& value); +}; +} // namespace Slic3r + +#endif // #define slic3r_EmbossStylesSerializable_hpp_ + diff --git a/src/slic3r/Utils/FontConfigHelp.cpp b/src/slic3r/Utils/FontConfigHelp.cpp new file mode 100644 index 0000000000..15beca07ba --- /dev/null +++ b/src/slic3r/Utils/FontConfigHelp.cpp @@ -0,0 +1,140 @@ +#include "FontConfigHelp.hpp" + +#ifdef EXIST_FONT_CONFIG_INCLUDE + +#include +#include +#include "libslic3r/Utils.hpp" + +using namespace Slic3r::GUI; + + +// @Vojta suggest to make static variable global +// Guard for finalize Font Config +// Will be finalized on application exit +// It seams that it NOT work +static std::optional finalize_guard; +// cache for Loading of the default configuration file and building information about the available fonts. +static FcConfig *fc = nullptr; + +std::string Slic3r::GUI::get_font_path(const wxFont &font, bool reload_fonts) +{ + if (!finalize_guard.has_value()) { + FcInit(); + fc = FcInitLoadConfigAndFonts(); + finalize_guard.emplace([]() { + // Some internal problem of Font config or other library use FC too(like wxWidget) + // fccache.c:795: FcCacheFini: Assertion `fcCacheChains[i] == NULL' failed. + //FcFini(); + FcConfigDestroy(fc); + }); + } else if (reload_fonts) { + FcConfigDestroy(fc); + fc = FcInitLoadConfigAndFonts(); + } + + if (fc == nullptr) return ""; + + wxString fontDesc = font.GetNativeFontInfoUserDesc(); + wxString faceName = font.GetFaceName(); + const wxScopedCharBuffer faceNameBuffer = faceName.ToUTF8(); + const char * fontFamily = faceNameBuffer; + + // Check font slant + int slant = FC_SLANT_ROMAN; + if (fontDesc.Find(wxS("Oblique")) != wxNOT_FOUND) + slant = FC_SLANT_OBLIQUE; + else if (fontDesc.Find(wxS("Italic")) != wxNOT_FOUND) + slant = FC_SLANT_ITALIC; + + // Check font weight + int weight = FC_WEIGHT_NORMAL; + if (fontDesc.Find(wxS("Book")) != wxNOT_FOUND) + weight = FC_WEIGHT_BOOK; + else if (fontDesc.Find(wxS("Medium")) != wxNOT_FOUND) + weight = FC_WEIGHT_MEDIUM; +#ifdef FC_WEIGHT_ULTRALIGHT + else if (fontDesc.Find(wxS("Ultra-Light")) != wxNOT_FOUND) + weight = FC_WEIGHT_ULTRALIGHT; +#endif + else if (fontDesc.Find(wxS("Light")) != wxNOT_FOUND) + weight = FC_WEIGHT_LIGHT; + else if (fontDesc.Find(wxS("Semi-Bold")) != wxNOT_FOUND) + weight = FC_WEIGHT_DEMIBOLD; +#ifdef FC_WEIGHT_ULTRABOLD + else if (fontDesc.Find(wxS("Ultra-Bold")) != wxNOT_FOUND) + weight = FC_WEIGHT_ULTRABOLD; +#endif + else if (fontDesc.Find(wxS("Bold")) != wxNOT_FOUND) + weight = FC_WEIGHT_BOLD; + else if (fontDesc.Find(wxS("Heavy")) != wxNOT_FOUND) + weight = FC_WEIGHT_BLACK; + + // Check font width + int width = FC_WIDTH_NORMAL; + if (fontDesc.Find(wxS("Ultra-Condensed")) != wxNOT_FOUND) + width = FC_WIDTH_ULTRACONDENSED; + else if (fontDesc.Find(wxS("Extra-Condensed")) != wxNOT_FOUND) + width = FC_WIDTH_EXTRACONDENSED; + else if (fontDesc.Find(wxS("Semi-Condensed")) != wxNOT_FOUND) + width = FC_WIDTH_SEMICONDENSED; + else if (fontDesc.Find(wxS("Condensed")) != wxNOT_FOUND) + width = FC_WIDTH_CONDENSED; + else if (fontDesc.Find(wxS("Ultra-Expanded")) != wxNOT_FOUND) + width = FC_WIDTH_ULTRAEXPANDED; + else if (fontDesc.Find(wxS("Extra-Expanded")) != wxNOT_FOUND) + width = FC_WIDTH_EXTRAEXPANDED; + else if (fontDesc.Find(wxS("Semi-Expanded")) != wxNOT_FOUND) + width = FC_WIDTH_SEMIEXPANDED; + else if (fontDesc.Find(wxS("Expanded")) != wxNOT_FOUND) + width = FC_WIDTH_EXPANDED; + + FcResult res; + FcPattern *matchPattern = FcPatternBuild(NULL, FC_FAMILY, FcTypeString, + (FcChar8 *) fontFamily, NULL); + ScopeGuard sg_mp([matchPattern]() { FcPatternDestroy(matchPattern); }); + + FcPatternAddInteger(matchPattern, FC_SLANT, slant); + FcPatternAddInteger(matchPattern, FC_WEIGHT, weight); + FcPatternAddInteger(matchPattern, FC_WIDTH, width); + + FcConfigSubstitute(NULL, matchPattern, FcMatchPattern); + FcDefaultSubstitute(matchPattern); + + FcPattern *resultPattern = FcFontMatch(NULL, matchPattern, &res); + if (resultPattern == nullptr) return ""; + ScopeGuard sg_rp([resultPattern]() { FcPatternDestroy(resultPattern); }); + + FcChar8 *fileName; + if (FcPatternGetString(resultPattern, FC_FILE, 0, &fileName) != + FcResultMatch) + return ""; + wxString fontFileName = wxString::FromUTF8((char *) fileName); + + if (fontFileName.IsEmpty()) return ""; + + // find full file path + wxFileName myFileName(fontFileName); + if (!myFileName.IsOk()) return ""; + + if (myFileName.IsRelative()) { + // Check whether the file is relative to the current working directory + if (!(myFileName.MakeAbsolute() && myFileName.FileExists())) { + return ""; + // File not found, search in given search paths + // wxString foundFileName = + // m_searchPaths.FindAbsoluteValidPath(fileName); if + // (!foundFileName.IsEmpty()) { + // myFileName.Assign(foundFileName); + //} + } + } + + if (!myFileName.FileExists() || !myFileName.IsFileReadable()) return ""; + + // File exists and is accessible + wxString fullFileName = myFileName.GetFullPath(); + return std::string(fullFileName.c_str()); +} + +#endif // EXIST_FONT_CONFIG_INCLUDE \ No newline at end of file diff --git a/src/slic3r/Utils/FontConfigHelp.hpp b/src/slic3r/Utils/FontConfigHelp.hpp new file mode 100644 index 0000000000..d2d7738767 --- /dev/null +++ b/src/slic3r/Utils/FontConfigHelp.hpp @@ -0,0 +1,25 @@ +#ifndef slic3r_FontConfigHelp_hpp_ +#define slic3r_FontConfigHelp_hpp_ + +#ifdef __linux__ +#define EXIST_FONT_CONFIG_INCLUDE +#endif + +#ifdef EXIST_FONT_CONFIG_INCLUDE +#include +namespace Slic3r::GUI { + +/// +/// initialize font config +/// Convert wx widget font to file path +/// inspired by wxpdfdoc - +/// https://github.com/utelle/wxpdfdoc/blob/5bdcdb9953327d06dc50ec312685ccd9bc8400e0/src/pdffontmanager.cpp +/// +/// Wx descriptor of font +/// flag to reinitialize font list +/// Font FilePath by FontConfig +std::string get_font_path(const wxFont &font, bool reload_fonts = false); + +} // namespace Slic3r +#endif // EXIST_FONT_CONFIG_INCLUDE +#endif // slic3r_FontConfigHelp_hpp_ diff --git a/src/slic3r/Utils/RaycastManager.cpp b/src/slic3r/Utils/RaycastManager.cpp new file mode 100644 index 0000000000..14c76722d9 --- /dev/null +++ b/src/slic3r/Utils/RaycastManager.cpp @@ -0,0 +1,131 @@ +#include "RaycastManager.hpp" +#include + +// include for earn camera +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Camera.hpp" + +using namespace Slic3r::GUI; + +void RaycastManager::actualize(const ModelObject *object, const ISkip *skip) +{ + // check if volume was removed + std::vector removed_casters(m_raycasters.size(), {true}); + // check if inscance was removed + std::vector removed_transf(m_transformations.size(), {true}); + + // actualize MeshRaycaster + for (const ModelVolume *volume : object->volumes) { + size_t oid = volume->id().id; + if (skip != nullptr && skip->skip(oid)) + continue; + auto item = std::find_if(m_raycasters.begin(), m_raycasters.end(), + [oid](const RaycastManager::Raycaster &it)->bool { + return oid == it.first; + }); + if (item == m_raycasters.end()) { + // add new raycaster +#if ENABLE_RAYCAST_PICKING + auto raycaster = std::make_unique(volume->get_mesh_shared_ptr()); +#else // !ENABLE_RAYCAST_PICKING + auto raycaster = std::make_unique(volume->mesh()); +#endif // ENABLE_RAYCAST_PICKING + m_raycasters.emplace_back(std::make_pair(oid, std::move(raycaster))); + } else { + size_t index = item - m_raycasters.begin(); + removed_casters[index] = false; + } + } + + // actualize transformation matrices + for (const ModelVolume *volume : object->volumes) { + if (skip != nullptr && skip->skip(volume->id().id)) continue; + const Transform3d &volume_tr = volume->get_matrix(); + for (const ModelInstance *instance : object->instances) { + const Transform3d &instrance_tr = instance->get_matrix(); + Transform3d transformation = instrance_tr * volume_tr; + // TODO: add SLA shift Z + // transformation.translation()(2) += m_sla_shift_z; + TrKey tr_key = std::make_pair(instance->id().id, volume->id().id); + auto item = std::find_if(m_transformations.begin(), + m_transformations.end(), + [&tr_key](const TrItem &it) -> bool { + return it.first == tr_key; + }); + if (item != m_transformations.end()) { + // actualize transformation all the time + item->second = transformation; + size_t index = item - m_transformations.begin(); + removed_transf[index] = false; + } else { + // add new transformation + m_transformations.emplace_back( + std::make_pair(tr_key, transformation)); + } + } + } + + // clean other raycasters + for (int i = removed_casters.size() - 1; i >= 0; --i) + if (removed_casters[i]) + m_raycasters.erase(m_raycasters.begin() + i); + + // clean other transformation + for (int i = removed_transf.size() - 1; i >= 0; --i) + if (removed_transf[i]) + m_transformations.erase(m_transformations.begin() + i); +} + +std::optional RaycastManager::unproject( + const Vec2d &mouse_pos, const Camera &camera, const ISkip *skip) const +{ + struct HitWithDistance: public Hit + { + double squared_distance; + HitWithDistance(double squared_distance, + const TrKey & key, + const SurfacePoint &surface_point) + : Hit(key, surface_point.position, surface_point.normal) + , squared_distance(squared_distance) + {} + }; + std::optional closest; + for (const auto &item : m_transformations) { + const TrKey &key = item.first; + size_t volume_id = key.second; + if (skip != nullptr && skip->skip(volume_id)) continue; + const Transform3d &transformation = item.second; + auto raycaster_it = + std::find_if(m_raycasters.begin(), m_raycasters.end(), + [volume_id](const RaycastManager::Raycaster &it) + -> bool { return volume_id == it.first; }); + if (raycaster_it == m_raycasters.end()) continue; + const MeshRaycaster &raycaster = *(raycaster_it->second); + SurfacePoint surface_point; + bool success = raycaster.unproject_on_mesh( + mouse_pos, transformation, camera, + surface_point.position, surface_point.normal); + if (!success) continue; + + Vec3d act_hit_tr = transformation * surface_point.position.cast(); + double squared_distance = (camera.get_position() - act_hit_tr).squaredNorm(); + if (closest.has_value() && + closest->squared_distance < squared_distance) + continue; + closest = HitWithDistance(squared_distance, key, surface_point); + } + + //if (!closest.has_value()) return {}; + return closest; +} + +Slic3r::Transform3d RaycastManager::get_transformation(const TrKey &tr_key) const { + auto item = std::find_if(m_transformations.begin(), + m_transformations.end(), + [&tr_key](const TrItem &it) -> bool { + return it.first == tr_key; + }); + if (item == m_transformations.end()) return Transform3d::Identity(); + return item->second; +} \ No newline at end of file diff --git a/src/slic3r/Utils/RaycastManager.hpp b/src/slic3r/Utils/RaycastManager.hpp new file mode 100644 index 0000000000..031cc6acd5 --- /dev/null +++ b/src/slic3r/Utils/RaycastManager.hpp @@ -0,0 +1,115 @@ +#ifndef slic3r_RaycastManager_hpp_ +#define slic3r_RaycastManager_hpp_ + +#include // unique_ptr +#include // unique_ptr +#include +#include "slic3r/GUI/MeshUtils.hpp" // MeshRaycaster +#include "libslic3r/Point.hpp" // Transform3d +#include "libslic3r/ObjectID.hpp" +#include "libslic3r/Model.hpp" // ModelObjectPtrs, ModelObject, ModelInstance, ModelVolume + +namespace Slic3r::GUI{ + +/// +/// Cast rays from camera to scene +/// Used for find hit point on model volume under mouse cursor +/// +class RaycastManager +{ + // ModelVolume.id + using Raycaster = std::pair >; + std::vector m_raycasters; + + // Key for transformation consist of unique volume and instance + // ModelInstance, ModelVolume + using TrKey = std::pair; + using TrItem = std::pair; + std::vector m_transformations; + + // should contain shared pointer to camera but it is not shared pointer so it need it every time when casts rays + +public: + class ISkip{ + public: + virtual ~ISkip() = default; + + /// + /// Condition to not process model volume + /// + /// ObjectID of model volume to not process + /// True on skip otherwise false + virtual bool skip(const size_t &model_volume_id) const { return false; } + }; + + /// + /// Actualize raycasters + transformation + /// Detection of removed object + /// Detection of removed instance + /// Detection of removed volume + /// + /// Model representation + /// Condifiton for skip actualization + void actualize(const ModelObject *object, const ISkip *skip = nullptr); + + // TODO: it is more general object move outside of this class + struct SurfacePoint + { + Vec3f position = Vec3f::Zero(); + Vec3f normal = Vec3f::UnitZ(); + SurfacePoint() = default; + SurfacePoint(Vec3f position, Vec3f normal) + : position(position), normal(normal) + {} + }; + + struct Hit: public SurfacePoint + { + TrKey tr_key; + Hit(TrKey tr_key, Vec3f position, Vec3f normal) + : SurfacePoint(position, normal), tr_key(tr_key) + {} + }; + + class SkipVolume: public ISkip + { + size_t volume_id; + public: + SkipVolume(size_t volume_id) : volume_id(volume_id) {} + bool skip(const size_t &model_volume_id) const override { return model_volume_id == volume_id; } + }; + + class AllowVolumes: public ISkip + { + std::vector allowed_id; + public: + AllowVolumes(std::vector allowed_id) : allowed_id(allowed_id) {} + bool skip(const size_t &model_volume_id) const override { + auto it = std::find(allowed_id.begin(), allowed_id.end(), model_volume_id); + return it == allowed_id.end(); + } + }; + + /// + /// Unproject on mesh by Mesh raycasters + /// Note: Function use current camera position from wxGetApp() + /// + /// Position of mouse on screen + /// Projection params + /// Define which caster will be skipped, null mean no skip + /// Position on surface, normal direction and transformation key, which define hitted object instance + std::optional unproject(const Vec2d &mouse_pos, + const Camera &camera, + const ISkip *skip = nullptr) const; + + /// + /// Getter on transformation + /// + /// Define transformation + /// Transformation for key + Transform3d get_transformation(const TrKey &tr_key) const; +}; + +} // namespace Slic3r::GUI + +#endif // slic3r_RaycastManager_hpp_ diff --git a/src/slic3r/Utils/WxFontUtils.cpp b/src/slic3r/Utils/WxFontUtils.cpp new file mode 100644 index 0000000000..1c9fa12b01 --- /dev/null +++ b/src/slic3r/Utils/WxFontUtils.cpp @@ -0,0 +1,342 @@ +#include "WxFontUtils.hpp" +#include +#include + +#if defined(__APPLE__) +#include +#include +#include // wxNativeFontInfo +#include +#elif defined(__linux__) +#include "slic3r/Utils/FontConfigHelp.hpp" +#endif + +using namespace Slic3r; +using namespace Slic3r::GUI; + +namespace { + +#ifdef __APPLE__ +static bool is_valid_ttf(std::string_view file_path) +{ + if (file_path.empty()) return false; + auto const pos_point = file_path.find_last_of('.'); + if (pos_point == std::string_view::npos) return false; + + // use point only after last directory delimiter + auto const pos_directory_delimiter = file_path.find_last_of("/\\"); + if (pos_directory_delimiter != std::string_view::npos && + pos_point < pos_directory_delimiter) + return false; // point is before directory delimiter + + // check count of extension chars + size_t extension_size = file_path.size() - pos_point; + if (extension_size >= 5) return false; // a lot of symbols for extension + if (extension_size <= 1) return false; // few letters for extension + + std::string_view extension = file_path.substr(pos_point + 1, + extension_size); + + // Because of MacOs - Courier, Geneva, Monaco + if (extension == std::string_view("dfont")) return false; + + return true; +} + +// get filepath from wxFont on Mac OsX +static std::string get_file_path(const wxFont& font) { + const wxNativeFontInfo *info = font.GetNativeFontInfo(); + if (info == nullptr) return {}; + CTFontDescriptorRef descriptor = info->GetCTFontDescriptor(); + CFURLRef typeref = (CFURLRef) + CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute); + if (typeref == NULL) return {}; + CFStringRef url = CFURLGetString(typeref); + if (url == NULL) return {}; + wxString file_uri; + wxCFTypeRef(url).GetValue(file_uri); + std::string file_path(wxURI::Unescape(file_uri).c_str()); + size_t start = std::string("file://").size(); + if (file_path.empty() || file_path.size() <= start) + return {}; + // remove prefix file:// + file_path = file_path.substr(start, file_path.size() - start); + return file_path; +} +#endif // __APPLE__ +} // namespace + +bool WxFontUtils::can_load(const wxFont &font) +{ + if (!font.IsOk()) return false; +#ifdef _WIN32 + return Emboss::can_load(font.GetHFONT()) != nullptr; +#elif defined(__APPLE__) + return true; + //return is_valid_ttf(get_file_path(font)); +#elif defined(__linux__) + return true; + // font config check file path take about 4000ms for chech them all + //std::string font_path = Slic3r::GUI::get_font_path(font); + //return !font_path.empty(); +#endif + return false; +} + +std::unique_ptr WxFontUtils::create_font_file(const wxFont &font) +{ +#ifdef _WIN32 + return Emboss::create_font_file(font.GetHFONT()); +#elif defined(__APPLE__) + std::string file_path = get_file_path(font); + if (!is_valid_ttf(file_path)) { + BOOST_LOG_TRIVIAL(error) << "Can not process font('" << get_human_readable_name(font) << "'), " + << "file in path('" << file_path << "') is not valid TTF."; + return nullptr; + } + return Emboss::create_font_file(file_path.c_str()); +#elif defined(__linux__) + std::string font_path = Slic3r::GUI::get_font_path(font); + if (font_path.empty()){ + BOOST_LOG_TRIVIAL(error) << "Can not read font('" << get_human_readable_name(font) << "'), " + << "file path is empty."; + return nullptr; + } + return Emboss::create_font_file(font_path.c_str()); +#else + // HERE is place to add implementation for another platform + // to convert wxFont to font data as windows or font file path as linux + return nullptr; +#endif +} + +EmbossStyle::Type WxFontUtils::get_actual_type() +{ +#ifdef _WIN32 + return EmbossStyle::Type::wx_win_font_descr; +#elif defined(__APPLE__) + return EmbossStyle::Type::wx_mac_font_descr; +#elif defined(__linux__) + return EmbossStyle::Type::wx_lin_font_descr; +#else + return EmbossStyle::Type::undefined; +#endif +} + +EmbossStyle WxFontUtils::create_emboss_style(const wxFont &font, const std::string& name) +{ + std::string name_item = name.empty()? get_human_readable_name(font) : name; + std::string fontDesc = store_wxFont(font); + EmbossStyle::Type type = get_actual_type(); + + // synchronize font property with actual font + FontProp font_prop; + + // The point size is defined as 1/72 of the Anglo-Saxon inch (25.4 mm): it + // is approximately 0.0139 inch or 352.8 um. But it is too small, so I + // decide use point size as mm for emboss + font_prop.size_in_mm = font.GetPointSize(); // *0.3528f; + font_prop.emboss = font_prop.size_in_mm / 2.f; + + WxFontUtils::update_property(font_prop, font); + return { name_item, fontDesc, type, font_prop }; +} + +// NOT working on linux GTK2 +// load font used by Operating system as default GUI +//EmbossStyle WxFontUtils::get_os_font() +//{ +// wxSystemFont system_font = wxSYS_DEFAULT_GUI_FONT; +// wxFont font = wxSystemSettings::GetFont(system_font); +// EmbossStyle es = create_emboss_style(font); +// es.name += std::string(" (OS default)"); +// return es; +//} + +std::string WxFontUtils::get_human_readable_name(const wxFont &font) +{ + if (!font.IsOk()) return "Font is NOT ok."; + // Face name is optional in wxFont + if (!font.GetFaceName().empty()) { + return std::string(font.GetFaceName().c_str()); + } else { + return std::string((font.GetFamilyString() + " " + + font.GetStyleString() + " " + + font.GetWeightString()) + .c_str()); + } +} + +std::string WxFontUtils::store_wxFont(const wxFont &font) +{ + // wxString os = wxPlatformInfo::Get().GetOperatingSystemIdName(); + wxString font_descriptor = font.GetNativeFontInfoDesc(); + return std::string(font_descriptor.c_str()); +} + +wxFont WxFontUtils::load_wxFont(const std::string &font_descriptor) +{ + wxString font_descriptor_wx(font_descriptor); + wxFont wx_font(font_descriptor_wx); + return wx_font; +} + +using TypeToFamily = boost::bimap; +const TypeToFamily WxFontUtils::type_to_family = + boost::assign::list_of + (wxFONTFAMILY_DEFAULT, "default") + (wxFONTFAMILY_DECORATIVE, "decorative") + (wxFONTFAMILY_ROMAN, "roman") + (wxFONTFAMILY_SCRIPT, "script") + (wxFONTFAMILY_SWISS, "swiss") + (wxFONTFAMILY_MODERN, "modern") + (wxFONTFAMILY_TELETYPE, "teletype"); + +using TypeToStyle = boost::bimap; +const TypeToStyle WxFontUtils::type_to_style = + boost::assign::list_of + (wxFONTSTYLE_ITALIC, "italic") + (wxFONTSTYLE_SLANT, "slant") + (wxFONTSTYLE_NORMAL, "normal"); + +using TypeToWeight = boost::bimap; +const TypeToWeight WxFontUtils::type_to_weight = + boost::assign::list_of + (wxFONTWEIGHT_THIN, "thin") + (wxFONTWEIGHT_EXTRALIGHT, "extraLight") + (wxFONTWEIGHT_LIGHT, "light") + (wxFONTWEIGHT_NORMAL, "normal") + (wxFONTWEIGHT_MEDIUM, "medium") + (wxFONTWEIGHT_SEMIBOLD, "semibold") + (wxFONTWEIGHT_BOLD, "bold") + (wxFONTWEIGHT_EXTRABOLD, "extraBold") + (wxFONTWEIGHT_HEAVY, "heavy") + (wxFONTWEIGHT_EXTRAHEAVY, "extraHeavy"); + +wxFont WxFontUtils::create_wxFont(const EmbossStyle &style) +{ + const FontProp &fp = style.prop; + double point_size = static_cast(fp.size_in_mm); + wxFontInfo info(point_size); + if (fp.family.has_value()) { + auto it = type_to_family.right.find(*fp.family); + if (it != type_to_family.right.end()) info.Family(it->second); + } + if (fp.face_name.has_value()) { + wxString face_name(*fp.face_name); + info.FaceName(face_name); + } + if (fp.style.has_value()) { + auto it = type_to_style.right.find(*fp.style); + if (it != type_to_style.right.end()) info.Style(it->second); + } + if (fp.weight.has_value()) { + auto it = type_to_weight.right.find(*fp.weight); + if (it != type_to_weight.right.end()) info.Weight(it->second); + } + + // Improve: load descriptor instead of store to font property to 3mf + // switch (es.type) { + // case EmbossStyle::Type::wx_lin_font_descr: + // case EmbossStyle::Type::wx_win_font_descr: + // case EmbossStyle::Type::wx_mac_font_descr: + // case EmbossStyle::Type::file_path: + // case EmbossStyle::Type::undefined: + // default: + //} + + wxFont wx_font(info); + // Check if exist font file + std::unique_ptr ff = create_font_file(wx_font); + if (ff == nullptr) return {}; + + return wx_font; +} + +void WxFontUtils::update_property(FontProp &font_prop, const wxFont &font) +{ + wxString wx_face_name = font.GetFaceName(); + std::string face_name((const char *) wx_face_name.ToUTF8()); + if (!face_name.empty()) font_prop.face_name = face_name; + + wxFontFamily wx_family = font.GetFamily(); + if (wx_family != wxFONTFAMILY_DEFAULT) { + auto it = type_to_family.left.find(wx_family); + if (it != type_to_family.left.end()) font_prop.family = it->second; + } + + wxFontStyle wx_style = font.GetStyle(); + if (wx_style != wxFONTSTYLE_NORMAL) { + auto it = type_to_style.left.find(wx_style); + if (it != type_to_style.left.end()) font_prop.style = it->second; + } + + wxFontWeight wx_weight = font.GetWeight(); + if (wx_weight != wxFONTWEIGHT_NORMAL) { + auto it = type_to_weight.left.find(wx_weight); + if (it != type_to_weight.left.end()) font_prop.weight = it->second; + } +} + +bool WxFontUtils::is_italic(const wxFont &font) { + wxFontStyle wx_style = font.GetStyle(); + return wx_style == wxFONTSTYLE_ITALIC || + wx_style == wxFONTSTYLE_SLANT; +} + +bool WxFontUtils::is_bold(const wxFont &font) { + wxFontWeight wx_weight = font.GetWeight(); + return wx_weight != wxFONTWEIGHT_NORMAL; +} + +std::unique_ptr WxFontUtils::set_italic(wxFont &font, const Emboss::FontFile &font_file) +{ + static std::vector italic_styles = { + wxFontStyle::wxFONTSTYLE_ITALIC, + wxFontStyle::wxFONTSTYLE_SLANT + }; + wxFontStyle orig_style = font.GetStyle(); + for (wxFontStyle style : italic_styles) { + font.SetStyle(style); + std::unique_ptr new_font_file = + WxFontUtils::create_font_file(font); + + // can create italic font? + if (new_font_file == nullptr) continue; + + // is still same font file pointer? + if (font_file == *new_font_file) continue; + + return new_font_file; + } + // There is NO italic font by wx + font.SetStyle(orig_style); + return nullptr; +} + +std::unique_ptr WxFontUtils::set_bold(wxFont &font, const Emboss::FontFile& font_file) +{ + static std::vector bold_weight = { + wxFontWeight::wxFONTWEIGHT_BOLD, + wxFontWeight::wxFONTWEIGHT_HEAVY, + wxFontWeight::wxFONTWEIGHT_EXTRABOLD, + wxFontWeight::wxFONTWEIGHT_EXTRAHEAVY + }; + wxFontWeight orig_weight = font.GetWeight(); + for (wxFontWeight weight : bold_weight) { + font.SetWeight(weight); + std::unique_ptr new_font_file = + WxFontUtils::create_font_file(font); + + // can create bold font file? + if (new_font_file == nullptr) continue; + + // is still same font file pointer? + if (font_file == *new_font_file) continue; + + return new_font_file; + } + // There is NO bold font by wx + font.SetWeight(orig_weight); + return nullptr; +} \ No newline at end of file diff --git a/src/slic3r/Utils/WxFontUtils.hpp b/src/slic3r/Utils/WxFontUtils.hpp new file mode 100644 index 0000000000..c470b27410 --- /dev/null +++ b/src/slic3r/Utils/WxFontUtils.hpp @@ -0,0 +1,71 @@ +#ifndef slic3r_WxFontUtils_hpp_ +#define slic3r_WxFontUtils_hpp_ + +#include +#include +#include +#include +#include +#include "libslic3r/Emboss.hpp" + +namespace Slic3r::GUI { + +// Help class to work with wx widget font object( wxFont ) +class WxFontUtils +{ +public: + // only static functions + WxFontUtils() = delete; + + // check if exist file for wxFont + // return pointer on data or nullptr when can't load + static bool can_load(const wxFont &font); + + // os specific load of wxFont + static std::unique_ptr create_font_file(const wxFont &font); + + static EmbossStyle::Type get_actual_type(); + static EmbossStyle create_emboss_style(const wxFont &font, const std::string& name = ""); + + static std::string get_human_readable_name(const wxFont &font); + + // serialize / deserialize font + static std::string store_wxFont(const wxFont &font); + static wxFont load_wxFont(const std::string &font_descriptor); + + // Try to create similar font, loaded from 3mf from different Computer + static wxFont create_wxFont(const EmbossStyle &style); + // update font property by wxFont - without emboss depth and font size + static void update_property(FontProp &font_prop, const wxFont &font); + + static bool is_italic(const wxFont &font); + static bool is_bold(const wxFont &font); + + /// + /// Set italic into wx font + /// When italic font is same as original return nullptr. + /// To not load font file twice on success is font_file returned. + /// + /// wx descriptor of font + /// file described in wx font + /// New created font fileon success otherwise nullptr + static std::unique_ptr set_italic(wxFont &font, const Slic3r::Emboss::FontFile &prev_font_file); + + /// + /// Set boldness into wx font + /// When bolded font is same as original return nullptr. + /// To not load font file twice on success is font_file returned. + /// + /// wx descriptor of font + /// file described in wx font + /// New created font fileon success otherwise nullptr + static std::unique_ptr set_bold(wxFont &font, const Slic3r::Emboss::FontFile &font_file); + + // convert wxFont types to string and vice versa + static const boost::bimap type_to_family; + static const boost::bimap type_to_style; + static const boost::bimap type_to_weight; +}; + +} // namespace Slic3r::GUI +#endif // slic3r_WxFontUtils_hpp_ diff --git a/tests/data/contour_ALIENATO.TTF_glyph_i.svg b/tests/data/contour_ALIENATO.TTF_glyph_i.svg new file mode 100644 index 0000000000..f1d5a130fa --- /dev/null +++ b/tests/data/contour_ALIENATO.TTF_glyph_i.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/data/points_close_to_line.svg b/tests/data/points_close_to_line.svg new file mode 100644 index 0000000000..61ee61e61a --- /dev/null +++ b/tests/data/points_close_to_line.svg @@ -0,0 +1,3 @@ + + + diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index b4ba8208ad..ae2474ad50 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable(${_TEST_NAME}_tests test_color.cpp test_config.cpp test_curve_fitting.cpp + test_cut_surface.cpp test_elephant_foot_compensation.cpp test_expolygon.cpp test_geometry.cpp @@ -29,6 +30,9 @@ add_executable(${_TEST_NAME}_tests test_png_io.cpp test_surface_mesh.cpp test_timeutils.cpp + test_quadric_edge_collapse.cpp + test_triangulation.cpp + test_emboss.cpp test_indexed_triangle_set.cpp test_astar.cpp test_jump_point_search.cpp diff --git a/tests/libslic3r/libslic3r_tests.cpp b/tests/libslic3r/libslic3r_tests.cpp index f4dcab42a0..bfd012680b 100644 --- a/tests/libslic3r/libslic3r_tests.cpp +++ b/tests/libslic3r/libslic3r_tests.cpp @@ -2,6 +2,11 @@ #include "libslic3r/Utils.hpp" +// bimap test +#include +#include +#include + namespace { TEST_CASE("sort_remove_duplicates", "[utils]") { @@ -38,4 +43,46 @@ TEST_CASE("string_printf", "[utils]") { } } +TEST_CASE("Bimap duplicity behavior") { + enum class number { + one = 1, + three = 3, + tri = 3 // ONLY alias + }; + + using BimapType = boost::bimap; + BimapType bimap = boost::assign::list_of + ("one", number::one) + ("three", number::three) + ("tri", number::tri) // no matter if it is there + ; + + const auto& to_type = bimap.left; + + auto item_number1 = to_type.find("one"); + REQUIRE(item_number1 != to_type.end()); + CHECK(item_number1->second == number::one); + + auto item_number3 = to_type.find("three"); + REQUIRE(item_number3 != to_type.end()); + CHECK(item_number3->second == number::three); + + // to_type.find("tri"); // not in map + + const auto &to_name = bimap.right; + + auto it1 = to_name.find(number::one); + REQUIRE(it1 != to_name.end()); + CHECK(it1->second == "one"); + + auto it2 = to_name.find(number::three); + REQUIRE(it2 != to_name.end()); + CHECK(it2->second == "three"); + + auto it3 = to_name.find(number::tri); + REQUIRE(it3 != to_name.end()); + REQUIRE(number::three == number::tri); + CHECK(it3->second == "three"); } + +} // end namespace diff --git a/tests/libslic3r/test_aabbindirect.cpp b/tests/libslic3r/test_aabbindirect.cpp index def349042e..3a93eb908f 100644 --- a/tests/libslic3r/test_aabbindirect.cpp +++ b/tests/libslic3r/test_aabbindirect.cpp @@ -107,6 +107,50 @@ TEST_CASE("Creating a several 2d lines, testing all lines in radius query", "[AA REQUIRE(indices.size() == 3); } +TEST_CASE("Find the closest point from ExPolys", "[ClosestPoint]") { + ////////////////////////////// + // 0 - 3 + // |Ex0| 0 - 3 + // | |p |Ex1| + // 1 - 2 | | + // 1 - 2 + //[0,0] + /////////////////// + ExPolygons ex_polys{ + /*Ex0*/ {{0, 4}, {0, 1}, {2, 1}, {2, 4}}, + /*Ex1*/ {{4, 3}, {4, 0}, {6, 0}, {6, 3}} + }; + Vec2d p{2.5, 3.5}; + + std::vector lines; + auto add_lines = [&lines](const Polygon& poly) { + for (const auto &line : poly.lines()) + lines.emplace_back( + line.a.cast(), + line.b.cast()); + }; + for (const ExPolygon &ex_poly : ex_polys) { + add_lines(ex_poly.contour); + for (const Polygon &hole : ex_poly.holes) + add_lines(hole); + } + AABBTreeIndirect::Tree<2, double> tree = + AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + + size_t hit_idx_out = std::numeric_limits::max(); + Vec2d hit_point_out; + [[maybe_unused]] double distance_sq = + AABBTreeLines::squared_distance_to_indexed_lines( + lines, tree, p, hit_idx_out, hit_point_out, 0.24/* < (0.5*0.5) */); + CHECK(hit_idx_out == std::numeric_limits::max()); + distance_sq = AABBTreeLines::squared_distance_to_indexed_lines( + lines, tree, p, hit_idx_out, hit_point_out, 0.26); + CHECK(hit_idx_out != std::numeric_limits::max()); + + //double distance = sqrt(distance_sq); + //const Linef &line = lines[hit_idx_out]; +} + #if 0 #include "libslic3r/EdgeGrid.hpp" #include diff --git a/tests/libslic3r/test_cut_surface.cpp b/tests/libslic3r/test_cut_surface.cpp new file mode 100644 index 0000000000..ffc762b865 --- /dev/null +++ b/tests/libslic3r/test_cut_surface.cpp @@ -0,0 +1,171 @@ +#include + +#include +#include // its_make_cube + its_merge + +using namespace Slic3r; +TEST_CASE("Cut character from surface", "[]") +{ + std::string font_path = std::string(TEST_DATA_DIR) + + "/../../resources/fonts/NotoSans-Regular.ttf"; + char letter = '%'; + float flatness = 2.; + unsigned int font_index = 0; // collection + double z_depth = 50.f; // projection size + + auto font = Emboss::create_font_file(font_path.c_str()); + REQUIRE(font != nullptr); + std::optional glyph = + Emboss::letter2glyph(*font, font_index, letter, flatness); + REQUIRE(glyph.has_value()); + ExPolygons shapes = glyph->shape; + REQUIRE(!shapes.empty()); + + Transform3d tr = Transform3d::Identity(); + tr.translate(Vec3d(0., 0., -z_depth)); + tr.scale(Emboss::SHAPE_SCALE); + Emboss::OrthoProject cut_projection(tr, Vec3d(0., 0., z_depth)); + + auto object = its_make_cube(782 - 49 + 50, 724 + 10 + 50, 5); + its_translate(object, Vec3f(49 - 25, -10 - 25, -40)); + auto cube2 = object; // copy + its_translate(cube2, Vec3f(100, -40, 7.5)); + its_merge(object, std::move(cube2)); + + std::vector objects{object}; + // Call core function for cut surface + auto surfaces = cut_surface(shapes, objects, cut_projection, 0.5); + CHECK(!surfaces.empty()); + + Emboss::OrthoProject projection(Transform3d::Identity(), + Vec3d(0.f, 0.f, 10.f)); + its_translate(surfaces, Vec3f(0.f, 0.f, 10)); + + indexed_triangle_set its = cut2model(surfaces, projection); + CHECK(!its.empty()); + // its_write_obj(its, "C:/data/temp/projected.obj"); +} + +//#define DEBUG_3MF +#ifdef DEBUG_3MF + +// Test load of 3mf +#include "libslic3r/Format/3mf.hpp" +#include "libslic3r/Model.hpp" + +static std::vector transform_volumes(ModelVolume *mv) { + const auto &volumes = mv->get_object()->volumes; + std::vector results; + results.reserve(volumes.size()); + + // Improve create object from part or use gl_volume + // Get first model part in object + for (const ModelVolume *v : volumes) { + if (v->id() == mv->id()) continue; + if (!v->is_model_part()) continue; + const TriangleMesh &tm = v->mesh(); + if (tm.empty()) continue; + if (tm.its.empty()) continue; + results.push_back(tm.its); // copy: indexed_triangle_set + indexed_triangle_set& its = results.back(); + its_transform(its,v->get_matrix()); + } + return results; +} + +static Emboss::OrthoProject create_projection_for_cut( + Transform3d tr, + double shape_scale, + const BoundingBox &shape_bb, + const std::pair &z_range) +{ + // create sure that emboss object is bigger than source object + const float safe_extension = 1.0f; + double min_z = z_range.first - safe_extension; + double max_z = z_range.second + safe_extension; + assert(min_z < max_z); + // range between min and max value + double projection_size = max_z - min_z; + Matrix3d transformation_for_vector = tr.linear(); + // Projection must be negative value. + // System of text coordinate + // X .. from left to right + // Y .. from bottom to top + // Z .. from text to eye + Vec3d untransformed_direction(0., 0., projection_size); + Vec3d project_direction = transformation_for_vector * untransformed_direction; + + // Projection is in direction from far plane + tr.translate(Vec3d(0., 0., min_z)); + + tr.scale(shape_scale); + // Text alignemnt to center 2D + Vec2d move = -(shape_bb.max + shape_bb.min).cast() / 2.; + // Vec2d move = -shape_bb.center().cast(); // not precisse + tr.translate(Vec3d(move.x(), move.y(), 0.)); + return Emboss::OrthoProject(tr, project_direction); +} + +TEST_CASE("CutSurface in 3mf", "[Emboss]") +{ + //std::string path_to_3mf = "C:/Users/Filip Sykala/Downloads/EmbossFromMultiVolumes.3mf"; + //int object_id = 0; + //int text_volume_id = 2; + + //std::string path_to_3mf = "C:/Users/Filip Sykala/Downloads/treefrog.3mf"; + //int object_id = 0; + //int text_volume_id = 1; + + std::string path_to_3mf = "C:/Users/Filip Sykala/Downloads/cube_test.3mf"; + int object_id = 1; + int text_volume_id = 2; + + Model model; + DynamicPrintConfig config; + ConfigSubstitutionContext ctxt{ForwardCompatibilitySubstitutionRule::Disable}; + CHECK(load_3mf(path_to_3mf.c_str(), config, ctxt, &model, false)); + CHECK(object_id >= 0); + CHECK((size_t)object_id < model.objects.size()); + ModelObject* mo = model.objects[object_id]; + CHECK(mo != nullptr); + CHECK(text_volume_id >= 0); + CHECK((size_t)text_volume_id < mo->volumes.size()); + ModelVolume *mv_text = mo->volumes[text_volume_id]; + CHECK(mv_text != nullptr); + CHECK(mv_text->text_configuration.has_value()); + TextConfiguration &tc = *mv_text->text_configuration; + /* // Need GUI to load font by wx + std::optional wx_font = GUI::WxFontUtils::load_wxFont(tc.style.path); + CHECK(wx_font.has_value()); + Emboss::FontFileWithCache ff(GUI::WxFontUtils::create_font_file(*wx_font)); + CHECK(ff.font_file != nullptr); + /*/ // end use GUI + // start use fake font + std::string font_path = std::string(TEST_DATA_DIR) + + "/../../resources/fonts/NotoSans-Regular.ttf"; + Emboss::FontFileWithCache ff(Emboss::create_font_file(font_path.c_str())); + // */ // end use fake font + CHECK(ff.has_value()); + std::vector its = transform_volumes(mv_text); + BoundingBoxf3 bb; + for (auto &i : its) bb.merge(Slic3r::bounding_box(i)); + + Transform3d cut_projection_tr = mv_text->get_matrix() * tc.fix_3mf_tr->inverse(); + Transform3d emboss_tr = cut_projection_tr.inverse(); + BoundingBoxf3 mesh_bb_tr = bb.transformed(emboss_tr); + + std::pair z_range{mesh_bb_tr.min.z(), mesh_bb_tr.max.z()}; + + FontProp fp = tc.style.prop; + ExPolygons shapes = Emboss::text2shapes(ff, tc.text.c_str(), fp); + double shape_scale = Emboss::get_shape_scale(fp, *ff.font_file); + + Emboss::OrthoProject projection = create_projection_for_cut( + cut_projection_tr, shape_scale, get_extents(shapes), z_range); + + float projection_ratio = -z_range.first / (z_range.second - z_range.first); + SurfaceCut cut = cut_surface(shapes, its, projection, projection_ratio); + its_write_obj(cut, "C:/data/temp/cutSurface/result_cut.obj"); +} + +#endif // DEBUG_3MF diff --git a/tests/libslic3r/test_emboss.cpp b/tests/libslic3r/test_emboss.cpp new file mode 100644 index 0000000000..6a523ed31a --- /dev/null +++ b/tests/libslic3r/test_emboss.cpp @@ -0,0 +1,1259 @@ +#include + +#include +#include // only debug visualization + +#include +#include +#include // for next_highest_power_of_2() +#include +using namespace Slic3r; + +namespace Private{ + +// calculate multiplication of ray dir to intersect - inspired by +// segment_segment_intersection when ray dir is normalized retur distance from +// ray point to intersection No value mean no intersection +std::optional ray_segment_intersection(const Vec2d &r_point, + const Vec2d &r_dir, + const Vec2d &s0, + const Vec2d &s1) +{ + auto denominate = [](const Vec2d &v0, const Vec2d &v1) -> double { + return v0.x() * v1.y() - v1.x() * v0.y(); + }; + + Vec2d segment_dir = s1 - s0; + double d = denominate(segment_dir, r_dir); + if (std::abs(d) < std::numeric_limits::epsilon()) + // Line and ray are collinear. + return {}; + + Vec2d s12 = s0 - r_point; + double s_number = denominate(r_dir, s12); + bool change_sign = false; + if (d < 0.) { + change_sign = true; + d = -d; + s_number = -s_number; + } + + if (s_number < 0. || s_number > d) + // Intersection outside of segment. + return {}; + + double r_number = denominate(segment_dir, s12); + if (change_sign) r_number = -r_number; + + if (r_number < 0.) + // Intersection before ray start. + return {}; + + return r_number / d; +} + +Vec2d get_intersection(const Vec2d & point, + const Vec2d & dir, + const std::array &triangle) +{ + std::optional t; + for (size_t i = 0; i < 3; ++i) { + size_t i2 = i + 1; + if (i2 == 3) i2 = 0; + if (!t.has_value()) { + t = ray_segment_intersection(point, dir, triangle[i], + triangle[i2]); + continue; + } + + // small distance could be preccission inconsistance + std::optional t2 = ray_segment_intersection(point, dir, + triangle[i], + triangle[i2]); + if (t2.has_value() && *t2 > *t) t = t2; + } + assert(t.has_value()); // Not found intersection. + return point + dir * (*t); +} + +Vec3d calc_hit_point(const igl::Hit & h, + const Vec3i & triangle, + const std::vector &vertices) +{ + double c1 = h.u; + double c2 = h.v; + double c0 = 1.0 - c1 - c2; + Vec3d v0 = vertices[triangle[0]].cast(); + Vec3d v1 = vertices[triangle[1]].cast(); + Vec3d v2 = vertices[triangle[2]].cast(); + return v0 * c0 + v1 * c1 + v2 * c2; +} + +Vec3d calc_hit_point(const igl::Hit &h, indexed_triangle_set &its) +{ + return calc_hit_point(h, its.indices[h.id], its.vertices); +} +} // namespace Private + +std::string get_font_filepath() { + std::string resource_dir = + std::string(TEST_DATA_DIR) + "/../../resources/"; + return resource_dir + "fonts/NotoSans-Regular.ttf"; +} + +#include "imgui/imstb_truetype.h" +TEST_CASE("Read glyph C shape from font, stb library calls ONLY", "[Emboss]") { + std::string font_path = get_font_filepath(); + char letter = 'C'; + + // Read font file + FILE *file = fopen(font_path.c_str(), "rb"); + REQUIRE(file != nullptr); + // find size of file + REQUIRE(fseek(file, 0L, SEEK_END) == 0); + size_t size = ftell(file); + REQUIRE(size != 0); + rewind(file); + std::vector buffer(size); + size_t count_loaded_bytes = fread((void *) &buffer.front(), 1, size, file); + REQUIRE(count_loaded_bytes == size); + + // Use stb true type library + int font_offset = stbtt_GetFontOffsetForIndex(buffer.data(), 0); + REQUIRE(font_offset >= 0); + stbtt_fontinfo font_info; + REQUIRE(stbtt_InitFont(&font_info, buffer.data(), font_offset) != 0); + int unicode_letter = (int) letter; + int glyph_index = stbtt_FindGlyphIndex(&font_info, unicode_letter); + REQUIRE(glyph_index != 0); + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(&font_info, glyph_index, &vertices); + CHECK(num_verts > 0); + free(vertices); +} + +#include +TEST_CASE("Convert glyph % to model", "[Emboss]") +{ + std::string font_path = get_font_filepath(); + unsigned int font_index = 0; // collection + char letter = '%'; + float flatness = 2.; + + auto font = Emboss::create_font_file(font_path.c_str()); + REQUIRE(font != nullptr); + + std::optional glyph = + Emboss::letter2glyph(*font, font_index, letter, flatness); + REQUIRE(glyph.has_value()); + + ExPolygons shape = glyph->shape; + REQUIRE(!shape.empty()); + + float z_depth = 1.f; + Emboss::ProjectZ projection(z_depth); + indexed_triangle_set its = Emboss::polygons2model(shape, projection); + + CHECK(!its.indices.empty()); +} + +//#define VISUALIZE +#ifdef VISUALIZE +TEST_CASE("Visualize glyph from font", "[Emboss]") +{ + std::string font_path = "C:/data/ALIENATO.TTF"; + std::string text = "i"; + + Emboss::FontFileWithCache font( + Emboss::create_font_file(font_path.c_str())); + REQUIRE(font.has_value()); + FontProp fp; + fp.size_in_mm = 8; + fp.emboss = 4; + ExPolygons shapes = Emboss::text2shapes(font, text.c_str(), fp); + + // char letter = 'i'; + // unsigned int font_index = 0; // collection + // float flatness = 5; + // auto glyph = Emboss::letter2glyph(*font.font_file, font_index, letter, + // flatness); ExPolygons shapes2 = glyph->shape; { SVG + //svg("C:/data/temp/stored_letter.svg", get_extents(shapes2)); + //svg.draw(shapes2); } // debug shape + + REQUIRE(!shapes.empty()); + //{ SVG svg("C:/data/temp/shapes.svg"); svg.draw(shapes); } // debug shape + + float z_depth = 100.f; + Emboss::ProjectZ projection(z_depth); + indexed_triangle_set its = Emboss::polygons2model(shapes, projection); + its_write_obj(its, "C:/data/temp/bad_glyph.obj"); + + CHECK(!its.indices.empty()); + TriangleMesh tm(its); + auto s = tm.stats(); +} +#endif // VISUALIZE + +#include "test_utils.hpp" +#include "nanosvg/nanosvg.h" // load SVG file +#include "libslic3r/NSVGUtils.hpp" +ExPolygons heal_and_check(const Polygons &polygons) +{ + Pointfs intersections_prev = intersection_points(polygons); + Points polygons_points = to_points(polygons); + Points duplicits_prev = collect_duplications(polygons_points); + + ExPolygons shape = Emboss::heal_shape(polygons); + + // Is default shape for unhealabled shape? + bool is_default_shape = + shape.size() == 1 && + shape.front().contour.points.size() == 4 && + shape.front().holes.size() == 1 && + shape.front().holes.front().points.size() == 4 ; + CHECK(!is_default_shape); + + Pointfs intersections = intersection_points(shape); + Points shape_points = to_points(shape); + Points duplicits = collect_duplications(shape_points); + //{ + // BoundingBox bb(polygons_points); + // // bb.scale(svg_scale); + // SVG svg("C:/data/temp/test_visualization.svg", bb); + // svg.draw(polygons, "gray"); // input + // svg.draw(shape, "green"); // output + + // Points pts; + // pts.reserve(intersections.size()); + // for (const Vec2d &intersection : intersections) + // pts.push_back(intersection.cast()); + // svg.draw(pts, "red", 10); + // svg.draw(duplicits, "orenge", 10); + //} + + CHECK(intersections.empty()); + CHECK(duplicits.empty()); + return shape; +} + +void scale(Polygons &polygons, double multiplicator) { + for (Polygon &polygon : polygons) + for (Point &p : polygon) p *= multiplicator; +} + +TEST_CASE("Heal of damaged polygons", "[Emboss]") +{ + // Shape loaded from svg is letter 'i' from font 'ALIENATE.TTF' + std::string file_name = "contour_ALIENATO.TTF_glyph_i.svg"; + std::string file_path = TEST_DATA_DIR PATH_SEPARATOR + file_name; + NSVGimage *image = nsvgParseFromFile(file_path.c_str(), "px", 96.0f); + Polygons polygons = NSVGUtils::to_polygons(image); + nsvgDelete(image); + + heal_and_check(polygons); + + Polygons scaled_shape = polygons; // copy + scale(scaled_shape, 1 / Emboss::SHAPE_SCALE); + heal_and_check(scaled_shape); + + // different scale + scale(scaled_shape, 10.); + heal_and_check(scaled_shape); + + // check reverse order of points + Polygons reverse_shape = polygons; + for (Polygon &p : reverse_shape) + std::reverse(p.points.begin(), p.points.end()); + heal_and_check(scaled_shape); + +#ifdef VISUALIZE + CHECK(false); +#endif // VISUALIZE +} + +TEST_CASE("Heal of points close to line", "[Emboss]") +{ + // Shape loaded from svg is letter 'i' from font 'ALIENATE.TTF' + std::string file_name = "points_close_to_line.svg"; + std::string file_path = TEST_DATA_DIR PATH_SEPARATOR + file_name; + NSVGimage *image = nsvgParseFromFile(file_path.c_str(), "px", 96.0f); + Polygons polygons = NSVGUtils::to_polygons(image); + nsvgDelete(image); + REQUIRE(polygons.size() == 1); + Polygon polygon = polygons.front(); + polygon.points.pop_back();// NSVG put first point as last one when polygon is closed + ExPolygons expoly({ExPolygon(polygon)}); + CHECK(Emboss::divide_segments_for_close_point(expoly, .6)); + //{ SVG svg("C:/data/temp/healed.svg"); svg.draw(expoly);} + CHECK(to_points(expoly).size() >= (to_points(polygon).size() + 2)); +} + +TEST_CASE("Convert text with glyph cache to model", "[Emboss]") +{ + std::string font_path = get_font_filepath(); + std::string text = +"Because Ford never learned to say his original name, \n\ +his father eventually died of shame, which is still \r\n\ +a terminal disease in some parts of the Galaxy.\n\r\ +The other kids at school nicknamed him Ix,\n\ +which in the language of Betelgeuse Five translates as\t\n\ +\"boy who is not able satisfactorily to explain what a Hrung is,\n\ +nor why it should choose to collapse on Betelgeuse Seven\"."; + float line_height = 10.f, depth = 2.f; + + auto font = Emboss::create_font_file(font_path.c_str()); + REQUIRE(font != nullptr); + + Emboss::FontFileWithCache ffwc(std::move(font)); + FontProp fp{line_height, depth}; + ExPolygons shapes = Emboss::text2shapes(ffwc, text.c_str(), fp); + REQUIRE(!shapes.empty()); + + Emboss::ProjectZ projection(depth); + indexed_triangle_set its = Emboss::polygons2model(shapes, projection); + CHECK(!its.indices.empty()); + //its_write_obj(its, "C:/data/temp/text.obj"); +} + +TEST_CASE("Test hit point", "[AABBTreeIndirect]") +{ + indexed_triangle_set its; + its.vertices = { + Vec3f(1, 1, 1), + Vec3f(2, 10, 2), + Vec3f(10, 0, 2), + }; + its.indices = {Vec3i(0, 2, 1)}; + auto tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( + its.vertices, its.indices); + + Vec3d ray_point(8, 1, 0); + Vec3d ray_dir(0, 0, 1); + igl::Hit hit; + AABBTreeIndirect::intersect_ray_first_hit(its.vertices, its.indices, tree, + ray_point, ray_dir, hit); + Vec3d hp = Private::calc_hit_point(hit, its); + CHECK(abs(hp.x() - ray_point.x()) < .1); + CHECK(abs(hp.y() - ray_point.y()) < .1); +} + +TEST_CASE("ray segment intersection", "[MeshBoolean]") +{ + Vec2d r_point(1, 1); + Vec2d r_dir(1, 0); + + // colinear + CHECK(!Private::ray_segment_intersection(r_point, r_dir, Vec2d(0, 0), Vec2d(2, 0)).has_value()); + CHECK(!Private::ray_segment_intersection(r_point, r_dir, Vec2d(2, 0), Vec2d(0, 0)).has_value()); + + // before ray + CHECK(!Private::ray_segment_intersection(r_point, r_dir, Vec2d(0, 0), Vec2d(0, 2)).has_value()); + CHECK(!Private::ray_segment_intersection(r_point, r_dir, Vec2d(0, 2), Vec2d(0, 0)).has_value()); + + // above ray + CHECK(!Private::ray_segment_intersection(r_point, r_dir, Vec2d(2, 2), Vec2d(2, 3)).has_value()); + CHECK(!Private::ray_segment_intersection(r_point, r_dir, Vec2d(2, 3), Vec2d(2, 2)).has_value()); + + // belove ray + CHECK(!Private::ray_segment_intersection(r_point, r_dir, Vec2d(2, 0), Vec2d(2, -1)).has_value()); + CHECK(!Private::ray_segment_intersection(r_point, r_dir, Vec2d(2, -1), Vec2d(2, 0)).has_value()); + + // intersection at [2,1] distance 1 + auto t1 = Private::ray_segment_intersection(r_point, r_dir, Vec2d(2, 0), Vec2d(2, 2)); + REQUIRE(t1.has_value()); + auto t2 = Private::ray_segment_intersection(r_point, r_dir, Vec2d(2, 2), Vec2d(2, 0)); + REQUIRE(t2.has_value()); + + CHECK(abs(*t1 - *t2) < std::numeric_limits::epsilon()); +} + +TEST_CASE("triangle intersection", "[]") +{ + Vec2d point(1, 1); + Vec2d dir(-1, 0); + std::array triangle = {Vec2d(0, 0), Vec2d(5, 0), Vec2d(0, 5)}; + Vec2d i = Private::get_intersection(point, dir, triangle); + CHECK(abs(i.x()) < std::numeric_limits::epsilon()); + CHECK(abs(i.y() - 1.) < std::numeric_limits::epsilon()); +} + +#ifndef __APPLE__ +#include +#include +#include +namespace fs = std::filesystem; +// Check function Emboss::is_italic that exist some italic and some non-italic font. +TEST_CASE("Italic check", "[Emboss]") +{ + std::queue dir_paths; +#ifdef _WIN32 + dir_paths.push("C:/Windows/Fonts"); +#elif defined(__linux__) + dir_paths.push("/usr/share/fonts"); +//#elif defined(__APPLE__) +// dir_paths.push("//System/Library/Fonts"); +#endif + bool exist_italic = false; + bool exist_non_italic = false; + while (!dir_paths.empty()) { + std::string dir_path = dir_paths.front(); + dir_paths.pop(); + for (const auto &entry : fs::directory_iterator(dir_path)) { + const fs::path &act_path = entry.path(); + if (entry.is_directory()) { + dir_paths.push(act_path.u8string()); + continue; + } + std::string ext = act_path.extension().u8string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (ext != ".ttf") continue; + std::string path_str = act_path.u8string(); + auto font_opt = Emboss::create_font_file(path_str.c_str()); + if (font_opt == nullptr) continue; + + unsigned int collection_number = 0; + if (Emboss::is_italic(*font_opt, collection_number)) + exist_italic = true; + else + exist_non_italic = true; + + if (exist_italic && exist_non_italic) break; + //std::cout << ((Emboss::is_italic(*font_opt)) ? "[yes] " : "[no ] ") << entry.path() << std::endl; + } + } + CHECK(exist_italic); + CHECK(exist_non_italic); +} +#endif // not __APPLE__ + +#include "libslic3r/CutSurface.hpp" +TEST_CASE("Cut surface", "[]") +{ + std::string font_path = get_font_filepath(); + char letter = '%'; + float flatness = 2.; + unsigned int font_index = 0; // collection + double z_depth = 50.; // projection size + + auto font = Emboss::create_font_file(font_path.c_str()); + REQUIRE(font != nullptr); + + std::optional glyph = Emboss::letter2glyph(*font, + font_index, + letter, + flatness); + REQUIRE(glyph.has_value()); + + ExPolygons shape = glyph->shape; + REQUIRE(!shape.empty()); + + Transform3d tr = Transform3d::Identity(); + tr.translate(Vec3d(0., 0., -z_depth)); + tr.scale(Emboss::SHAPE_SCALE); + Emboss::OrthoProject cut_projection(tr, Vec3d(0., 0., z_depth)); + + auto object = its_make_cube(782 - 49 + 50, 724 + 10 + 50, 5); + its_translate(object, Vec3f(49 - 25, -10 - 25, -40)); + auto cube2 = object; // copy + its_translate(cube2, Vec3f(100, -40, 7.5)); + its_merge(object, std::move(cube2)); + + auto surfaces = cut_surface(shape, {object}, cut_projection, 0); + CHECK(!surfaces.empty()); + + Emboss::OrthoProject projection(Transform3d::Identity(), Vec3d(0., 0., 10.)); + its_translate(surfaces, Vec3f(0., 0., 10.)); + + indexed_triangle_set its = cut2model(surfaces, projection); + CHECK(!its.empty()); + //its_write_obj(its, "C:/data/temp/projected.obj"); +} + +#include +#include +#include +TEST_CASE("UndoRedo serialization", "[Emboss]") +{ + TextConfiguration tc; + tc.text = "Dovede-li se člověk zasmát sám sobě, nevyjde ze smíchu po celý život."; + EmbossStyle& es = tc.style; + es.name = "Seneca"; + es.path = "Simply the best"; + es.type = EmbossStyle::Type::file_path; + FontProp &fp = es.prop; + fp.angle = 100.; + fp.distance = 10.; + fp.char_gap = 1; + fp.use_surface = true; + tc.fix_3mf_tr = Transform3d::Identity(); + + std::stringstream ss; // any stream can be used + { + cereal::BinaryOutputArchive oarchive(ss); // Create an output archive + + oarchive(tc); + } // archive goes out of scope, ensuring all contents are flushed + + TextConfiguration tc_loaded; + { + cereal::BinaryInputArchive iarchive(ss); // Create an input archive + iarchive(tc_loaded); + } + CHECK(tc.style == tc_loaded.style); + CHECK(tc.text == tc_loaded.text); + CHECK(tc.fix_3mf_tr.has_value() == tc_loaded.fix_3mf_tr.has_value()); +} + + +#include +#include +#include +#include + +/// +/// Distiguish point made by shape(Expolygon) +/// Referencing an ExPolygon contour plus a vertex base of the contour. +/// Used for adressing Vertex of mesh created by extrude ExPolygons +/// +struct ShapesVertexId { + // Index of an ExPolygon in ExPolygons. + int32_t expoly{ -1 }; + + // Index of a contour in ExPolygon. + // 0 - outer contour, >0 - hole + int32_t contour{ -1 }; + + // Base of the zero'th point of a contour in text mesh. + // There are two vertices (front and rear) created for each contour, + // thus there are 2x more vertices in text mesh than the number of contour points. + int32_t vertex_base{ -1 }; +}; + +/// +/// IntersectingElemnt +/// +/// Adress polygon inside of ExPolygon +/// Keep information about source of vertex: +/// - from face (one of 2 possible) +/// - from edge (one of 2 possible) +/// +/// V1~~~~V2 +/// : f1 /| +/// : / | +/// : /e1| +/// : / |e2 +/// :/ f2 | +/// V1'~~~V2' +/// +/// | .. edge +/// / .. edge +/// : .. foreign edge - neighbor +/// ~ .. no care edge - idealy should not cross model +/// V1,V1' .. projected 2d point to 3d +/// V2,V2' .. projected 2d point to 3d +/// +/// f1 .. text_face_1 (triangle face made by side of shape contour) +/// f2 .. text_face_2 +/// e1 .. text_edge_1 (edge on side of face made by side of shape contour) +/// e2 .. text_edge_2 +/// +/// +struct IntersectingElemnt +{ + // Index into vector of ShapeVertexId + // describe point on shape contour + int32_t vertex_index{-1}; + + // index of point in Polygon contour + int32_t point_index{-1}; + + // vertex or edge ID, where edge ID is the index of the source point. + // There are 4 consecutive indices generated for a single glyph edge: + // 0th - 1st text edge (straight) + // 1th - 1st text face + // 2nd - 2nd text edge (diagonal) + // 3th - 2nd text face + // Type of intersecting element from extruded shape( 3d ) + enum class Type { + edge_1 = 0, + face_1 = 1, + edge_2 = 2, + face_2 = 3, + + undefined = 4 + } type = Type::undefined; +}; + +namespace Slic3r::MeshBoolean::cgal2 { + + namespace CGALProc = CGAL::Polygon_mesh_processing; + namespace CGALParams = CGAL::Polygon_mesh_processing::parameters; + + using EpicKernel = CGAL::Exact_predicates_inexact_constructions_kernel; + using _EpicMesh = CGAL::Surface_mesh; +// using EpecKernel = CGAL::Exact_predicates_exact_constructions_kernel; +// using _EpecMesh = CGAL::Surface_mesh; + + using CGALMesh = _EpicMesh; + + /// + /// Convert triangle mesh model to CGAL Surface_mesh + /// Add property map for source face index + /// + /// Model + /// Property map name for store conversion from CGAL face to index to its + /// CGAL mesh - half edge mesh + CGALMesh to_cgal(const indexed_triangle_set &its, + const std::string &face_map_name) + { + CGALMesh result; + if (its.empty()) return result; + + const std::vector &V = its.vertices; + const std::vector &F = its.indices; + + // convert from CGAL face to its face + auto face_map = result.add_property_map(face_map_name).first; + + size_t vertices_count = V.size(); + size_t edges_count = (F.size() * 3) / 2; + size_t faces_count = F.size(); + result.reserve(vertices_count, edges_count, faces_count); + + for (auto &v : V) + result.add_vertex(typename CGALMesh::Point{v.x(), v.y(), v.z()}); + + using VI = typename CGALMesh::Vertex_index; + for (auto &f : F) + { + auto fid = result.add_face(VI(f(0)), VI(f(1)), VI(f(2))); + // index of face in source triangle mesh + int32_t index = static_cast(&f - &F.front()); + face_map[fid] = index; + } + + return result; + } + + /// + /// Covert 2d shape (e.g. Glyph) to CGAL model + /// + /// 2d shape to project + /// Define transformation 2d point into 3d + /// Identify shape + /// Name of property map to store conversion from edge to contour + /// Name of property map to store conversion from face to contour + /// Identify point on shape contour + /// CGAL model of extruded shape + CGALMesh to_cgal(const ExPolygons &shape, + const Slic3r::Emboss::IProjection &projection, + int32_t shape_id, + const std::string &edge_shape_map_name, + const std::string &face_shape_map_name, + std::vector &contour_indices) + { + CGALMesh result; + if (shape.empty()) return result; + + auto edge_shape_map = result.add_property_map(edge_shape_map_name).first; + auto face_shape_map = result.add_property_map(face_shape_map_name).first; + + std::vector indices; + auto insert_contour = [&projection, &indices , &result, &contour_indices, &edge_shape_map, &face_shape_map](const Polygon& polygon, int32_t iexpoly, int32_t id) { + indices.clear(); + indices.reserve(polygon.points.size() * 2); + size_t num_vertices_old = result.number_of_vertices(); + int32_t vertex_index = static_cast(contour_indices.size()); + contour_indices.push_back({iexpoly, id, int32_t(num_vertices_old) }); + for (const Point& p2 : polygon.points) { + auto p = projection.create_front_back(p2); + auto vi = result.add_vertex(typename CGALMesh::Point{ p.first.x(), p.first.y(), p.first.z() }); + assert((size_t)vi == indices.size() + num_vertices_old); + indices.emplace_back(vi); + vi = result.add_vertex(typename CGALMesh::Point{ p.second.x(), p.second.y(), p.second.z() }); + assert((size_t)vi == indices.size() + num_vertices_old); + indices.emplace_back(vi); + } + int32_t contour_index = 0; + for (int32_t i = 0; i < int32_t(indices.size()); i += 2) { + int32_t j = (i + 2) % int32_t(indices.size()); + auto find_edge = [&result](CGALMesh::Face_index fi, CGALMesh::Vertex_index from, CGALMesh::Vertex_index to) { + CGALMesh::Halfedge_index hi = result.halfedge(fi); + for (; result.target(hi) != to; hi = result.next(hi)); + assert(result.source(hi) == from); + assert(result.target(hi) == to); + return hi; + }; + auto fi = result.add_face(indices[i], indices[i + 1], indices[j]); + edge_shape_map[result.edge(find_edge(fi, indices[i], indices[i + 1]))] = + IntersectingElemnt{vertex_index, contour_index, IntersectingElemnt::Type::edge_1}; + face_shape_map[fi] = + IntersectingElemnt{vertex_index, contour_index, IntersectingElemnt::Type::face_1}; + edge_shape_map[result.edge(find_edge(fi, indices[i + 1], indices[j]))] = + IntersectingElemnt{vertex_index, contour_index, IntersectingElemnt::Type::edge_2}; + face_shape_map[result.add_face(indices[j], indices[i + 1], indices[j + 1])] = + IntersectingElemnt{vertex_index, contour_index, IntersectingElemnt::Type::face_2}; + ++contour_index; + } + }; + + size_t count_point = count_points(shape); + result.reserve(result.number_of_vertices() + 2 * count_point, result.number_of_edges() + 4 * count_point, result.number_of_faces() + 2 * count_point); + + // Identify polygon + // (contour_id > 0) are holes + for (const auto &s : shape) { + size_t contour_id = 0; + insert_contour(s.contour, shape_id, contour_id++); + for (const Polygon &hole : s.holes) + insert_contour(hole, shape_id, contour_id++); + ++shape_id; + } + return result; + } +} +#include "libslic3r/TriangleMesh.hpp" + +//// 1 //// + +// Question store(1) Or calculate on demand(2) ?? +// (1) type: vector > +// (1) Needs recalculation when merge and propagation togewther with its +// (2) Could appear surface mistakes(need calc - all half edges) +// (2) NO need of trace cut outline and connect it with letter conture points + +/// +/// Cut surface shape from source model +/// +/// Input source mesh +/// Input 2d shape to cut from surface +/// Define transformation from 2d to 3d +/// Cutted surface, Its do not represent Volume +indexed_triangle_set cut_shape(const indexed_triangle_set &source, + const ExPolygon &shape, + const Emboss::IProjection &projection) +{ + // NOT implemented yet + return {}; +} + +/// +/// Cut surface shape from source model +/// +/// Input source mesh +/// Input 2d shape to cut from surface +/// Define transformation from 2d to 3d +/// Cutted surface, Its do not represent Volume +indexed_triangle_set cut_shape(const indexed_triangle_set &source, + const ExPolygons &shapes, + const Emboss::IProjection &projection) +{ + indexed_triangle_set result; + for (const ExPolygon &shape : shapes) + its_merge(result, cut_shape(source, shape, projection)); + return result; +} + +using MyMesh = Slic3r::MeshBoolean::cgal2::CGALMesh; + +// First Idea //// 1 //// +// Use source model to modify ONLY surface of text ModelVolume + +// Second Idea +// Store original its inside of text configuration[optional] +// Cause problem with next editation of object -> cut, simplify, Netfabb, Hollow, ...(transform original vertices) +TEST_CASE("Emboss extrude cut", "[Emboss-Cut]") +{ + std::string font_path = get_font_filepath(); + unsigned int font_index = 0; // collection + char letter = '%'; + float flatness = 2.; + + auto font = Emboss::create_font_file(font_path.c_str()); + REQUIRE(font != nullptr); + + std::optional glyph = + Emboss::letter2glyph(*font, font_index, letter, flatness); + REQUIRE(glyph.has_value()); + + ExPolygons shape = glyph->shape; + REQUIRE(!shape.empty()); + + float z_depth = 50.f; + Emboss::ProjectZ projection(z_depth); + +#if 0 + indexed_triangle_set text = Emboss::polygons2model(shape, projection); + BoundingBoxf3 bbox = bounding_box(text); + + CHECK(!text.indices.empty()); +#endif + + auto cube = its_make_cube(782 - 49 + 50, 724 + 10 + 50, 5); + its_translate(cube, Vec3f(49 - 25, -10 - 25, 2.5)); + auto cube2 = cube; +// its_translate(cube2, Vec3f(0, 0, 40)); + its_translate(cube2, Vec3f(100, -40, 40)); + its_merge(cube, std::move(cube2)); + + //cube = its_make_sphere(350., 1.); + //for (auto &face : cube2.indices) + // for (int i = 0; i < 3; ++ i) + // face(i) += int(cube.vertices.size()); + //append(cube.vertices, cube2.vertices); + //append(cube.indices, cube2.indices); + + using MyMesh = Slic3r::MeshBoolean::cgal2::CGALMesh; + + // name of CGAL property map for store source object face id - index into its.indices + std::string face_map_name = "f:face_map"; + std::string face_type_map_name = "f:type"; + // identify glyph for intersected vertex + std::string vert_shape_map_name = "v:glyph_id"; + MyMesh cgal_object = MeshBoolean::cgal2::to_cgal(cube, face_map_name); + auto face_map = cgal_object.property_map(face_map_name).first; + auto vert_shape_map = cgal_object.add_property_map(vert_shape_map_name).first; + + std::string edge_shape_map_name = "e:glyph_id"; + std::string face_shape_map_name = "f:glyph_id"; + std::vector glyph_contours; + + MyMesh cgal_shape = MeshBoolean::cgal2::to_cgal(shape, projection, 0, edge_shape_map_name, face_shape_map_name, glyph_contours); + + auto edge_shape_map = cgal_shape.property_map(edge_shape_map_name).first; + auto face_shape_map = cgal_shape.property_map(face_shape_map_name).first; + + // bool map for affected edge + using d_prop_bool = CGAL::dynamic_edge_property_t; + using ecm_it = boost::property_map::SMPM; + using EcmType = CGAL::internal::Dynamic; + EcmType ecm = get(d_prop_bool(), cgal_object); + + struct Visitor { + const MyMesh &object; + const MyMesh &shape; + // Properties of the shape mesh: + MyMesh::Property_map edge_shape_map; + MyMesh::Property_map face_shape_map; + // Properties of the object mesh. + MyMesh::Property_map face_map; + MyMesh::Property_map vert_shape_map; + + typedef boost::graph_traits GT; + typedef typename GT::face_descriptor face_descriptor; + typedef typename GT::halfedge_descriptor halfedge_descriptor; + typedef typename GT::vertex_descriptor vertex_descriptor; + + int32_t source_face_id = -1; + void before_subface_creations(face_descriptor f_old, MyMesh& mesh) + { + assert(&mesh == &object); + source_face_id = face_map[f_old]; + } + // it is called multiple times for one source_face_id + void after_subface_created(face_descriptor f_new, MyMesh &mesh) + { + assert(&mesh == &object); + assert(source_face_id != -1); + face_map[f_new] = source_face_id; + } + + std::vector intersection_point_glyph; + + // Intersecting an edge hh_edge from tm_edge with a face hh_face of tm_face. + void intersection_point_detected( + // ID of the intersection point, starting at 0. Ids are consecutive. + std::size_t i_id, + // Dimension of a simplex part of face(hh_face) that is intersected by hh_edge: + // 0 for vertex: target(hh_face) + // 1 for edge: hh_face + // 2 for the interior of face: face(hh_face) + int simplex_dimension, + // Edge of tm_edge, see edge_source_coplanar_with_face & edge_target_coplanar_with_face whether any vertex of hh_edge is coplanar with face(hh_face). + halfedge_descriptor hh_edge, + // Vertex, halfedge or face of tm_face intersected by hh_edge, see comment at simplex_dimension. + halfedge_descriptor hh_face, + // Mesh containing hh_edge + const MyMesh& tm_edge, + // Mesh containing hh_face + const MyMesh& tm_face, + // source(hh_edge) is coplanar with face(hh_face). + bool edge_source_coplanar_with_face, + // target(hh_edge) is coplanar with face(hh_face). + bool edge_target_coplanar_with_face) + { + if (i_id <= intersection_point_glyph.size()) { + intersection_point_glyph.reserve(Slic3r::next_highest_power_of_2(i_id + 1)); + intersection_point_glyph.resize(i_id + 1); + } + + const IntersectingElemnt* glyph = nullptr; + if (&tm_face == &shape) { + assert(&tm_edge == &object); + switch (simplex_dimension) { + case 1: + // edge x edge intersection + glyph = &edge_shape_map[shape.edge(hh_face)]; + break; + case 2: + // edge x face intersection + glyph = &face_shape_map[shape.face(hh_face)]; + break; + default: + assert(false); + } + if (edge_source_coplanar_with_face) + vert_shape_map[object.source(hh_edge)] = *glyph; + if (edge_target_coplanar_with_face) + vert_shape_map[object.target(hh_edge)] = *glyph; + } else { + assert(&tm_edge == &shape && &tm_face == &object); + assert(!edge_source_coplanar_with_face); + assert(!edge_target_coplanar_with_face); + glyph = &edge_shape_map[shape.edge(hh_edge)]; + if (simplex_dimension == 0) + vert_shape_map[object.target(hh_face)] = *glyph; + } + intersection_point_glyph[i_id] = glyph; + } + + void new_vertex_added(std::size_t node_id, vertex_descriptor vh, const MyMesh &tm) + { + assert(&tm == &object); + assert(node_id < intersection_point_glyph.size()); + const IntersectingElemnt * glyph = intersection_point_glyph[node_id]; + assert(glyph != nullptr); + assert(glyph->vertex_index != -1); + assert(glyph->point_index != -1); + vert_shape_map[vh] = glyph ? *glyph : IntersectingElemnt{}; + } + + void after_subface_creations(MyMesh&) {} + void before_subface_created(MyMesh&) {} + void before_edge_split(halfedge_descriptor /* h */, MyMesh& /* tm */) {} + void edge_split(halfedge_descriptor /* hnew */, MyMesh& /* tm */) {} + void after_edge_split() {} + void add_retriangulation_edge(halfedge_descriptor /* h */, MyMesh& /* tm */) {} + } visitor{cgal_object, cgal_shape, edge_shape_map, face_shape_map, + face_map, vert_shape_map}; + + const auto& p = CGAL::Polygon_mesh_processing::parameters::throw_on_self_intersection(false).visitor(visitor).edge_is_constrained_map(ecm); + const auto& q = CGAL::Polygon_mesh_processing::parameters::do_not_modify(true); + // CGAL::Polygon_mesh_processing::corefine(cgal_object, cgalcube2, p, p); + + CGAL::Polygon_mesh_processing::corefine(cgal_object, cgal_shape, p, q); + + enum class SideType { + // face inside of the cutted shape + inside, + // face outside of the cutted shape + outside, + // face without constrained edge (In or Out) + not_constrained + }; + auto side_type_map = cgal_object.add_property_map("f:side").first; + for (auto fi : cgal_object.faces()) { + SideType side_type = SideType::not_constrained; + auto hi_end = cgal_object.halfedge(fi); + auto hi = hi_end; + do { + CGAL::SM_Edge_index edge_index = cgal_object.edge(hi); + // is edge new created - constrained? + if (get(ecm, edge_index)) { + // This face has a constrained edge. + IntersectingElemnt shape_from = vert_shape_map[cgal_object.source(hi)]; + IntersectingElemnt shape_to = vert_shape_map[cgal_object.target(hi)]; + assert(shape_from.vertex_index != -1); + assert(shape_from.vertex_index == shape_to.vertex_index); + assert(shape_from.point_index != -1); + assert(shape_to.point_index != -1); + + const ShapesVertexId &vertex_index = glyph_contours[shape_from.vertex_index]; + const ExPolygon &expoly = shape[vertex_index.expoly]; + const Polygon &contour = vertex_index.contour == 0 ? expoly.contour : expoly.holes[vertex_index.contour - 1]; + bool is_inside = false; + + // 4 type + // index into contour + int32_t i_from = shape_from.point_index; + int32_t i_to = shape_to.point_index; + if (i_from == i_to && shape_from.type == shape_to.type) { + + const auto &p = cgal_object.point(cgal_object.target(cgal_object.next(hi))); + + int i = i_from * 2; + int j = (i_from + 1 == int(contour.size())) ? 0 : i + 2; + + i += vertex_index.vertex_base; + j += vertex_index.vertex_base; + + auto abcp = + shape_from.type == IntersectingElemnt::Type::face_1 ? + CGAL::orientation( + cgal_shape.point(CGAL::SM_Vertex_index(i)), + cgal_shape.point(CGAL::SM_Vertex_index(i + 1)), + cgal_shape.point(CGAL::SM_Vertex_index(j)), p) : + // shape_from.type == IntersectingElemnt::Type::face_2 + CGAL::orientation( + cgal_shape.point(CGAL::SM_Vertex_index(j)), + cgal_shape.point(CGAL::SM_Vertex_index(i + 1)), + cgal_shape.point(CGAL::SM_Vertex_index(j + 1)), + p); + is_inside = abcp == CGAL::POSITIVE; + } else if (i_from < i_to || (i_from == i_to && shape_from.type < shape_to.type)) { + bool is_last = i_from == 0 && static_cast(i_to + 1) == contour.size(); + if (!is_last) is_inside = true; + } else { // i_from > i_to || (i_from == i_to && shape_from.type > shape_to.type) + bool is_last = i_to == 0 && static_cast(i_from + 1) == contour.size(); + if (is_last) is_inside = true; + } + + if (is_inside) { + // Is this face oriented towards p or away from p? + const auto &a = cgal_object.point(cgal_object.source(hi)); + const auto &b = cgal_object.point(cgal_object.target(hi)); + const auto &c = cgal_object.point(cgal_object.target(cgal_object.next(hi))); + //FIXME prosim nahrad skutecnou projekci. + //projection.project() + const auto p = a + MeshBoolean::cgal2::EpicKernel::Vector_3(0, 0, 10); + auto abcp = CGAL::orientation(a, b, c, p); + if (abcp == CGAL::POSITIVE) + side_type = SideType::inside; + else + is_inside = false; + } + if (!is_inside) side_type = SideType::outside; + break; + } + // next half edge index inside of face + hi = cgal_object.next(hi); + } while (hi != hi_end); + side_type_map[fi] = side_type; + } + + // debug output + auto face_colors = cgal_object.add_property_map("f:color").first; + for (auto fi : cgal_object.faces()) { + auto &color = face_colors[fi]; + switch (side_type_map[fi]) { + case SideType::inside: color = CGAL::Color{255, 0, 0}; break; + case SideType::outside: color = CGAL::Color{255, 0, 255}; break; + case SideType::not_constrained: color = CGAL::Color{0, 255, 0}; break; + } + } + CGAL::IO::write_OFF("c:\\data\\temp\\constrained.off", cgal_object); + + // Seed fill the other faces inside the region. + for (Visitor::face_descriptor fi : cgal_object.faces()) { + if (side_type_map[fi] != SideType::not_constrained) continue; + + // check if neighbor face is inside + Visitor::halfedge_descriptor hi = cgal_object.halfedge(fi); + Visitor::halfedge_descriptor hi_end = hi; + + bool has_inside_neighbor = false; + std::vector queue; + do { + Visitor::face_descriptor fi_opposite = cgal_object.face(cgal_object.opposite(hi)); + SideType side = side_type_map[fi_opposite]; + if (side == SideType::inside) { + has_inside_neighbor = true; + } else if (side == SideType::not_constrained) { + queue.emplace_back(fi_opposite); + } + hi = cgal_object.next(hi); + } while (hi != hi_end); + if (!has_inside_neighbor) continue; + side_type_map[fi] = SideType::inside; + while (!queue.empty()) { + Visitor::face_descriptor fi = queue.back(); + queue.pop_back(); + // Do not fill twice + if (side_type_map[fi] == SideType::inside) continue; + side_type_map[fi] = SideType::inside; + + // check neighbor triangle + Visitor::halfedge_descriptor hi = cgal_object.halfedge(fi); + Visitor::halfedge_descriptor hi_end = hi; + do { + Visitor::face_descriptor fi_opposite = cgal_object.face(cgal_object.opposite(hi)); + SideType side = side_type_map[fi_opposite]; + if (side == SideType::not_constrained) + queue.emplace_back(fi_opposite); + hi = cgal_object.next(hi); + } while (hi != hi_end); + } + } + + // debug output + for (auto fi : cgal_object.faces()) { + auto &color = face_colors[fi]; + switch (side_type_map[fi]) { + case SideType::inside: color = CGAL::Color{255, 0, 0}; break; + case SideType::outside: color = CGAL::Color{255, 0, 255}; break; + case SideType::not_constrained: color = CGAL::Color{0, 255, 0}; break; + } + } + CGAL::IO::write_OFF("c:\\data\\temp\\filled.off", cgal_object); + + // Mapping of its_extruded faces to source faces. + enum class FaceState : int8_t { + Unknown = -1, + Unmarked = -2, + UnmarkedSplit = -3, + Marked = -4, + MarkedSplit = -5, + UnmarkedEmitted = -6, + }; + std::vector face_states(cube.indices.size(), FaceState::Unknown); + for (auto fi_seed : cgal_object.faces()) { + FaceState &state = face_states[face_map[fi_seed]]; + bool is_face_inside = side_type_map[fi_seed] == SideType::inside; + switch (state) { + case FaceState::Unknown: + state = is_face_inside ? FaceState::Marked : FaceState::Unmarked; + break; + case FaceState::Unmarked: + case FaceState::UnmarkedSplit: + state = is_face_inside ? FaceState::MarkedSplit : FaceState::UnmarkedSplit; + break; + case FaceState::Marked: + case FaceState::MarkedSplit: + state = FaceState::MarkedSplit; + break; + default: + assert(false); + } + } + + indexed_triangle_set its_extruded; + its_extruded.indices.reserve(cgal_object.number_of_faces()); + its_extruded.vertices.reserve(cgal_object.number_of_vertices()); + // Mapping of its_extruded vertices (original and offsetted) to cgalcuble's vertices. + std::vector> map_vertices(cgal_object.number_of_vertices(), std::pair{-1, -1}); + + Vec3f extrude_dir { 0, 0, 5.f }; + for (auto fi : cgal_object.faces()) { + const int32_t source_face_id = face_map[fi]; + const FaceState state = face_states[source_face_id]; + assert(state == FaceState::Unmarked || state == FaceState::UnmarkedSplit || state == FaceState::UnmarkedEmitted || + state == FaceState::Marked || state == FaceState::MarkedSplit); + if (state == FaceState::UnmarkedEmitted) continue; // Already emitted. + + if (state == FaceState::Unmarked || + state == FaceState::UnmarkedSplit) { + // Just copy the unsplit source face. + const Vec3i source_vertices = cube.indices[source_face_id]; + Vec3i target_vertices; + for (int i = 0; i < 3; ++i) { + target_vertices(i) = map_vertices[source_vertices(i)].first; + if (target_vertices(i) == -1) { + map_vertices[source_vertices(i)].first = target_vertices(i) = int(its_extruded.vertices.size()); + its_extruded.vertices.emplace_back(cube.vertices[source_vertices(i)]); + } + } + its_extruded.indices.emplace_back(target_vertices); + face_states[source_face_id] = FaceState::UnmarkedEmitted; + continue; // revert modification + } + + auto hi = cgal_object.halfedge(fi); + auto hi_prev = cgal_object.prev(hi); + auto hi_next = cgal_object.next(hi); + const Vec3i source_vertices{ + int((std::size_t)cgal_object.target(hi)), + int((std::size_t)cgal_object.target(hi_next)), + int((std::size_t)cgal_object.target(hi_prev)) }; + Vec3i target_vertices; + if (side_type_map[fi] != SideType::inside) { + // Copy the face. + Vec3i target_vertices; + for (int i = 0; i < 3; ++ i) { + target_vertices(i) = map_vertices[source_vertices(i)].first; + if (target_vertices(i) == -1) { + map_vertices[source_vertices(i)].first = target_vertices(i) = int(its_extruded.vertices.size()); + const auto &p = cgal_object.point(cgal_object.target(hi)); + its_extruded.vertices.emplace_back(p.x(), p.y(), p.z()); + } + hi = cgal_object.next(hi); + } + its_extruded.indices.emplace_back(target_vertices); + continue; // copy splitted triangle + } + + // Extrude the face. Neighbor edges separating extruded face from + // non-extruded face will be extruded. + bool boundary_vertex[3] = {false, false, false}; + Vec3i target_vertices_extruded{-1, -1, -1}; + for (int i = 0; i < 3; ++i) { + if (side_type_map[cgal_object.face(cgal_object.opposite(hi))] != SideType::inside) + // Edge separating extruded / non-extruded region. + boundary_vertex[i] = true; + hi = cgal_object.next(hi); + } + + for (int i = 0; i < 3; ++i) { + target_vertices_extruded(i) = map_vertices[source_vertices(i)].second; + if (target_vertices_extruded(i) == -1) { + map_vertices[source_vertices(i)].second = + target_vertices_extruded(i) = int( + its_extruded.vertices.size()); + const auto &p = cgal_object.point(cgal_object.target(hi)); + its_extruded.vertices.emplace_back( + Vec3f{float(p.x()), float(p.y()), float(p.z())} + + extrude_dir); + } + if (boundary_vertex[i]) { + target_vertices(i) = map_vertices[source_vertices(i)].first; + if (target_vertices(i) == -1) { + map_vertices[source_vertices(i)].first = target_vertices( + i) = int(its_extruded.vertices.size()); + const auto &p = cgal_object.point(cgal_object.target(hi)); + its_extruded.vertices.emplace_back(p.x(), p.y(), p.z()); + } + } + hi = cgal_object.next(hi); + } + its_extruded.indices.emplace_back(target_vertices_extruded); + // Add the sides. + for (int i = 0; i < 3; ++i) { + int j = (i + 1) % 3; + assert(target_vertices_extruded[i] != -1 && + target_vertices_extruded[j] != -1); + if (boundary_vertex[i] && boundary_vertex[j]) { + assert(target_vertices[i] != -1 && target_vertices[j] != -1); + its_extruded.indices.emplace_back( + Vec3i{target_vertices[i], target_vertices[j], + target_vertices_extruded[i]}); + its_extruded.indices.emplace_back( + Vec3i{target_vertices_extruded[i], target_vertices[j], + target_vertices_extruded[j]}); + } + } + } + + its_write_obj(its_extruded, "c:\\data\\temp\\text-extruded.obj"); + + indexed_triangle_set edges_its; + std::vector edges_its_colors; + for (auto ei : cgal_object.edges()) + if (cgal_object.is_valid(ei)) { + const auto &p1 = cgal_object.point(cgal_object.vertex(ei, 0)); + const auto &p2 = cgal_object.point(cgal_object.vertex(ei, 1)); + bool constrained = get(ecm, ei); + Vec3f color = constrained ? Vec3f{ 1.f, 0, 0 } : Vec3f{ 0, 1., 0 }; + edges_its.indices.emplace_back(Vec3i(edges_its.vertices.size(), edges_its.vertices.size() + 1, edges_its.vertices.size() + 2)); + edges_its.vertices.emplace_back(Vec3f(p1.x(), p1.y(), p1.z())); + edges_its.vertices.emplace_back(Vec3f(p2.x(), p2.y(), p2.z())); + edges_its.vertices.emplace_back(Vec3f(p2.x(), p2.y(), p2.z() + 0.001)); + edges_its_colors.emplace_back(color); + edges_its_colors.emplace_back(color); + edges_its_colors.emplace_back(color); + } + its_write_obj(edges_its, edges_its_colors, "c:\\data\\temp\\corefined-edges.obj"); + +// MeshBoolean::cgal::minus(cube, cube2); + +// REQUIRE(!MeshBoolean::cgal::does_self_intersect(cube)); +} diff --git a/tests/libslic3r/test_indexed_triangle_set.cpp b/tests/libslic3r/test_indexed_triangle_set.cpp index 0bb85b7ed0..b6aad9dcf4 100644 --- a/tests/libslic3r/test_indexed_triangle_set.cpp +++ b/tests/libslic3r/test_indexed_triangle_set.cpp @@ -215,50 +215,6 @@ bool is_similar(const indexed_triangle_set &from, return true; } -TEST_CASE("Reduce one edge by Quadric Edge Collapse", "[its]") -{ - indexed_triangle_set its; - its.vertices = {Vec3f(-1.f, 0.f, 0.f), Vec3f(0.f, 1.f, 0.f), - Vec3f(1.f, 0.f, 0.f), Vec3f(0.f, 0.f, 1.f), - // vertex to be removed - Vec3f(0.9f, .1f, -.1f)}; - its.indices = {Vec3i(1, 0, 3), Vec3i(2, 1, 3), Vec3i(0, 2, 3), - Vec3i(0, 1, 4), Vec3i(1, 2, 4), Vec3i(2, 0, 4)}; - // edge to remove is between vertices 2 and 4 on trinagles 4 and 5 - - indexed_triangle_set its_ = its; // copy - // its_write_obj(its, "tetrhedron_in.obj"); - uint32_t wanted_count = its.indices.size() - 1; - its_quadric_edge_collapse(its, wanted_count); - // its_write_obj(its, "tetrhedron_out.obj"); - CHECK(its.indices.size() == 4); - CHECK(its.vertices.size() == 4); - - for (size_t i = 0; i < 3; i++) { - CHECK(its.indices[i] == its_.indices[i]); - } - - for (size_t i = 0; i < 4; i++) { - if (i == 2) continue; - CHECK(its.vertices[i] == its_.vertices[i]); - } - - const Vec3f &v = its.vertices[2]; // new vertex - const Vec3f &v2 = its_.vertices[2]; // moved vertex - const Vec3f &v4 = its_.vertices[4]; // removed vertex - for (size_t i = 0; i < 3; i++) { - bool is_between = (v[i] < v4[i] && v[i] > v2[i]) || - (v[i] > v4[i] && v[i] < v2[i]); - CHECK(is_between); - } - CompareConfig cfg; - cfg.max_average_distance = 0.014f; - cfg.max_distance = 0.75f; - - CHECK(is_similar(its, its_, cfg)); - CHECK(is_similar(its_, its, cfg)); -} - #include "test_utils.hpp" TEST_CASE("Simplify mesh by Quadric edge collapse to 5%", "[its]") { @@ -282,30 +238,3 @@ TEST_CASE("Simplify mesh by Quadric edge collapse to 5%", "[its]") CHECK(is_similar(its, mesh.its, cfg)); } -bool exist_triangle_with_twice_vertices(const std::vector& indices) -{ - for (const auto &face : indices) - if (face[0] == face[1] || - face[0] == face[2] || - face[1] == face[2]) return true; - return false; -} - -TEST_CASE("Simplify trouble case", "[its]") -{ - TriangleMesh tm = load_model("simplification.obj"); - REQUIRE_FALSE(tm.empty()); - float max_error = std::numeric_limits::max(); - uint32_t wanted_count = 0; - its_quadric_edge_collapse(tm.its, wanted_count, &max_error); - CHECK(!exist_triangle_with_twice_vertices(tm.its.indices)); -} - -TEST_CASE("Simplified cube should not be empty.", "[its]") -{ - auto its = its_make_cube(1, 2, 3); - float max_error = std::numeric_limits::max(); - uint32_t wanted_count = 0; - its_quadric_edge_collapse(its, wanted_count, &max_error); - CHECK(!its.indices.empty()); -} diff --git a/tests/libslic3r/test_meshboolean.cpp b/tests/libslic3r/test_meshboolean.cpp index f1582fe3be..7587677519 100644 --- a/tests/libslic3r/test_meshboolean.cpp +++ b/tests/libslic3r/test_meshboolean.cpp @@ -23,3 +23,29 @@ TEST_CASE("CGAL and TriangleMesh conversions", "[MeshBoolean]") { REQUIRE(! MeshBoolean::cgal::does_self_intersect(M)); } + +Vec3d calc_normal(const Vec3i &triangle, const std::vector &vertices) +{ + Vec3d v0 = vertices[triangle[0]].cast(); + Vec3d v1 = vertices[triangle[1]].cast(); + Vec3d v2 = vertices[triangle[2]].cast(); + // n = triangle normal + Vec3d n = (v1 - v0).cross(v2 - v0); + n.normalize(); + return n; +} + +TEST_CASE("Add TriangleMeshes", "[MeshBoolean]") +{ + TriangleMesh tm1 = make_sphere(1.6, 1.6); + size_t init_size = tm1.its.indices.size(); + Vec3f move(5, -3, 7); + move.normalize(); + tm1.translate(0.3 * move); + //its_write_obj(tm1.its, "tm1.obj"); + TriangleMesh tm2 = make_cube(1., 1., 1.); + //its_write_obj(tm2.its, "tm2.obj"); + MeshBoolean::cgal::plus(tm1, tm2); + //its_write_obj(tm1.its, "test_add.obj"); + CHECK(tm1.its.indices.size() > init_size); +} diff --git a/tests/libslic3r/test_polygon.cpp b/tests/libslic3r/test_polygon.cpp index 7774b44a6a..c1e1c3b733 100644 --- a/tests/libslic3r/test_polygon.cpp +++ b/tests/libslic3r/test_polygon.cpp @@ -210,3 +210,34 @@ SCENARIO("Simplify polygon", "[Polygon]") } } } + +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/ExPolygonsIndex.hpp" +TEST_CASE("Indexing expolygons", "[ExPolygon]") +{ + ExPolygons expolys{ + ExPolygon{Polygon{{0, 0}, {10, 0}, {0, 5}}, Polygon{{4, 3}, {6, 3}, {5, 2}}}, + ExPolygon{Polygon{{100, 0}, {110, 0}, {100, 5}}, Polygon{{104, 3}, {106, 3}, {105, 2}}} + }; + Points points = to_points(expolys); + Lines lines = to_lines(expolys); + Linesf linesf = to_linesf(expolys); + ExPolygonsIndices ids(expolys); + REQUIRE(points.size() == lines.size()); + REQUIRE(points.size() == linesf.size()); + REQUIRE(points.size() == ids.get_count()); + for (size_t i = 0; i < ids.get_count(); i++) { + ExPolygonsIndex id = ids.cvt(i); + const ExPolygon &expoly = expolys[id.expolygons_index]; + const Polygon &poly = id.is_contour() ? expoly.contour : expoly.holes[id.hole_index()]; + const Points &pts = poly.points; + const Point &p = pts[id.point_index]; + CHECK(points[i] == p); + CHECK(lines[i].a == p); + CHECK(linesf[i].a.cast() == p); + CHECK(ids.cvt(id) == i); + const Point &p_b = ids.is_last_point(id) ? pts.front() : pts[id.point_index + 1]; + CHECK(lines[i].b == p_b); + CHECK(linesf[i].b.cast() == p_b); + } +} diff --git a/tests/libslic3r/test_quadric_edge_collapse.cpp b/tests/libslic3r/test_quadric_edge_collapse.cpp new file mode 100644 index 0000000000..3a20f80f19 --- /dev/null +++ b/tests/libslic3r/test_quadric_edge_collapse.cpp @@ -0,0 +1,304 @@ +#include +#include + +#include +#include // its - indexed_triangle_set +#include "libslic3r/AABBTreeIndirect.hpp" // is similar + +using namespace Slic3r; + +namespace Private { + +struct Similarity +{ + float max_distance = 0.f; + float average_distance = 0.f; + + Similarity() = default; + Similarity(float max_distance, float average_distance) + : max_distance(max_distance), average_distance(average_distance) + {} +}; + +// border for our algorithm with frog_leg model and decimation to 5% +Similarity frog_leg_5(0.32f, 0.043f); + +Similarity get_similarity(const indexed_triangle_set &from, + const indexed_triangle_set &to) +{ + // create ABBTree + auto tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set( + from.vertices, from.indices); + float sum_distance = 0.f; + + float max_distance = 0.f; + auto collect_distances = [&](const Vec3f &surface_point) { + size_t hit_idx; + Vec3f hit_point; + float distance2 = + AABBTreeIndirect::squared_distance_to_indexed_triangle_set( + from.vertices, from.indices, tree, surface_point, hit_idx, + hit_point); + float distance = sqrt(distance2); + if (max_distance < distance) max_distance = distance; + sum_distance += distance; + }; + + for (const Vec3f &vertex : to.vertices) { collect_distances(vertex); } + for (const Vec3i &t : to.indices) { + Vec3f center(0, 0, 0); + for (size_t i = 0; i < 3; ++i) { center += to.vertices[t[i]] / 3; } + collect_distances(center); + } + + size_t count = to.vertices.size() + to.indices.size(); + float average_distance = sum_distance / count; + + std::cout << "max_distance = " << max_distance << ", average_distance = " << average_distance << std::endl; + return Similarity(max_distance, average_distance); +} + +void is_better_similarity(const indexed_triangle_set &its_first, + const indexed_triangle_set &its_second, + const Similarity & compare) +{ + Similarity s1 = get_similarity(its_first, its_second); + Similarity s2 = get_similarity(its_second, its_first); + + CHECK(s1.average_distance < compare.average_distance); + CHECK(s1.max_distance < compare.max_distance); + CHECK(s2.average_distance < compare.average_distance); + CHECK(s2.max_distance < compare.max_distance); +} + +void is_worse_similarity(const indexed_triangle_set &its_first, + const indexed_triangle_set &its_second, + const Similarity & compare) +{ + Similarity s1 = get_similarity(its_first, its_second); + Similarity s2 = get_similarity(its_second, its_first); + + if (s1.max_distance < compare.max_distance && + s2.max_distance < compare.max_distance) + CHECK(false); +} + +bool exist_triangle_with_twice_vertices(const std::vector &indices) +{ + for (const auto &face : indices) + if (face[0] == face[1] || face[0] == face[2] || face[1] == face[2]) + return true; + return false; +} + +} // namespace Private + +TEST_CASE("Reduce one edge by Quadric Edge Collapse", "[its]") +{ + indexed_triangle_set its; + its.vertices = {Vec3f(-1.f, 0.f, 0.f), Vec3f(0.f, 1.f, 0.f), + Vec3f(1.f, 0.f, 0.f), Vec3f(0.f, 0.f, 1.f), + // vertex to be removed + Vec3f(0.9f, .1f, -.1f)}; + its.indices = {Vec3i(1, 0, 3), Vec3i(2, 1, 3), Vec3i(0, 2, 3), + Vec3i(0, 1, 4), Vec3i(1, 2, 4), Vec3i(2, 0, 4)}; + // edge to remove is between vertices 2 and 4 on trinagles 4 and 5 + + indexed_triangle_set its_ = its; // copy + // its_write_obj(its, "tetrhedron_in.obj"); + uint32_t wanted_count = its.indices.size() - 1; + its_quadric_edge_collapse(its, wanted_count); + // its_write_obj(its, "tetrhedron_out.obj"); + CHECK(its.indices.size() == 4); + CHECK(its.vertices.size() == 4); + + for (size_t i = 0; i < 3; i++) { + CHECK(its.indices[i] == its_.indices[i]); + } + + for (size_t i = 0; i < 4; i++) { + if (i == 2) continue; + CHECK(its.vertices[i] == its_.vertices[i]); + } + + const Vec3f &v = its.vertices[2]; // new vertex + const Vec3f &v2 = its_.vertices[2]; // moved vertex + const Vec3f &v4 = its_.vertices[4]; // removed vertex + for (size_t i = 0; i < 3; i++) { + bool is_between = (v[i] < v4[i] && v[i] > v2[i]) || + (v[i] > v4[i] && v[i] < v2[i]); + CHECK(is_between); + } + Private::Similarity max_similarity(0.75f, 0.014f); + Private::is_better_similarity(its, its_, max_similarity); +} + +static bool is_equal(const std::vector &v1, + const std::vector &v2, + float epsilon = std::numeric_limits::epsilon()) +{ + // is same count? + if (v1.size() != v2.size()) return false; + + // check all v1 vertices + for (const auto &v1_ : v1) { + auto is_equal = [&v1_, epsilon](const auto &v2_) { + for (size_t i = 0; i < 3; i++) + if (fabs(v1_[i] - v2_[i]) > epsilon) + return false; + return true; + }; + // is v1 vertex in v2 vertices? + if(std::find_if(v2.begin(), v2.end(), is_equal) == v2.end()) return false; + } + return true; +} + +TEST_CASE("Reduce to one triangle by Quadric Edge Collapse", "[its]") +{ + // !!! Not work (no manifold - open edges{0-1, 1-2, 2-4, 4-5, 5-3, 3-0}): + /////////////image//// + // * 5 // + // |\ // + // | \ // + // 3 *--* 4 // + // | /|\ // + // |/ | \ // + // 0 *--*--* 2 // + // 1 // + ////////////////////// + // all triangles are on a plane therefore quadric is zero and + // when reduce edge between vertices 3 and 4 new vertex lay on vertex 3 not 4 !!! + + indexed_triangle_set its; + its.vertices = {Vec3f(0.f, 0.f, 0.f), Vec3f(1.f, 0.f, 0.f), + Vec3f(2.f, 0.f, 0.f), Vec3f(0.f, 1.f, 0.f), + Vec3f(1.f, 1.f, 0.f), Vec3f(0.f, 2.f, 0.f)}; + its.indices = {Vec3i(0, 1, 4), Vec3i(1, 2, 4), Vec3i(0, 4, 3), + Vec3i(3, 4, 5)}; + std::vector triangle_vertices = {its.vertices[0], + its.vertices[2], + its.vertices[5]}; + + uint32_t wanted_count = 1; + its_quadric_edge_collapse(its, wanted_count); + // result should be one triangle made of vertices 0, 2, 5 + + // NOT WORK + //CHECK(its.indices.size() == wanted_count); + //// check all triangle vertices + //CHECK(is_equal(its.vertices, triangle_vertices)); +} + +TEST_CASE("Reduce to one tetrahedron by Quadric Edge Collapse", "[its]") +{ + // Extend previous test to tetrahedron to make it manifold + indexed_triangle_set its; + its.vertices = { + Vec3f(0.f, 0.f, 0.f), Vec3f(1.f, 0.f, 0.f), Vec3f(2.f, 0.f, 0.f), + Vec3f(0.f, 1.f, 0.f), Vec3f(1.f, 1.f, 0.f), + Vec3f(0.f, 2.f, 0.f) + // tetrahedron extetion + , Vec3f(0.f, 0.f, -2.f) + }; + std::vector tetrahedron_vertices = {its.vertices[0], + its.vertices[2], + its.vertices[5], + // tetrahedron extetion + its.vertices[6]}; + its.indices = {Vec3i(0, 1, 4), Vec3i(1, 2, 4), Vec3i(0, 4, 3), Vec3i(3, 4, 5), + // tetrahedron extetion + Vec3i(4, 2, 6), Vec3i(5, 4, 6), Vec3i(3, 5, 6), Vec3i(0, 3, 6), Vec3i(1, 0, 6), Vec3i(2, 1, 6) + }; + uint32_t wanted_count = 4; + + //its_write_obj(its, "tetrhedron_in.obj"); + its_quadric_edge_collapse(its, wanted_count); + //its_write_obj(its, "tetrhedron_out.obj"); + + // result should be tetrahedron + CHECK(its.indices.size() == wanted_count); + // check all tetrahedron vertices + CHECK(is_equal(its.vertices, tetrahedron_vertices)); +} + +TEST_CASE("Simplify frog_legs.obj to 5% by Quadric edge collapse", "[its][quadric_edge_collapse]") +{ + TriangleMesh mesh = load_model("frog_legs.obj"); + double original_volume = its_volume(mesh.its); + uint32_t wanted_count = mesh.its.indices.size() * 0.05; + REQUIRE_FALSE(mesh.empty()); + indexed_triangle_set its = mesh.its; // copy + float max_error = std::numeric_limits::max(); + its_quadric_edge_collapse(its, wanted_count, &max_error); + // its_write_obj(its, "frog_legs_qec.obj"); + CHECK(its.indices.size() <= wanted_count); + double volume = its_volume(its); + CHECK(fabs(original_volume - volume) < 33.); + + Private::is_better_similarity(mesh.its, its, Private::frog_leg_5); +} + +#include +TEST_CASE("Simplify frog_legs.obj to 5% by IGL/qslim", "[]") +{ + std::string obj_filename = "frog_legs.obj"; + TriangleMesh mesh = load_model(obj_filename); + REQUIRE_FALSE(mesh.empty()); + indexed_triangle_set &its = mesh.its; + //double original_volume = its_volume(its); + uint32_t wanted_count = its.indices.size() * 0.05; + + Eigen::MatrixXd V(its.vertices.size(), 3); + Eigen::MatrixXi F(its.indices.size(), 3); + for (size_t j = 0; j < its.vertices.size(); ++j) { + Vec3d vd = its.vertices[j].cast(); + for (int i = 0; i < 3; ++i) V(j, i) = vd(i); + } + + for (size_t j = 0; j < its.indices.size(); ++j) { + const auto &f = its.indices[j]; + for (int i = 0; i < 3; ++i) F(j, i) = f(i); + } + + size_t max_m = wanted_count; + Eigen::MatrixXd U; + Eigen::MatrixXi G; + Eigen::VectorXi J, I; + CHECK(igl::qslim(V, F, max_m, U, G, J, I)); + + // convert to its + indexed_triangle_set its_out; + its_out.vertices.reserve(U.size()/3); + its_out.indices.reserve(G.size()/3); + size_t U_size = U.size() / 3; + for (size_t i = 0; i < U_size; i++) + its_out.vertices.emplace_back(U(i, 0), U(i, 1), U(i, 2)); + size_t G_size = G.size() / 3; + for (size_t i = 0; i < G_size; i++) + its_out.indices.emplace_back(G(i, 0), G(i, 1), G(i, 2)); + + // check if algorithm is still worse than our + Private::is_worse_similarity(its_out, its, Private::frog_leg_5); + // its_out, its --> avg_distance: 0.0351217, max_distance 0.364316 + // its, its_out --> avg_distance: 0.0412358, max_distance 0.238913 +} + +TEST_CASE("Simplify trouble case", "[its]") +{ + TriangleMesh tm = load_model("simplification.obj"); + REQUIRE_FALSE(tm.empty()); + float max_error = std::numeric_limits::max(); + uint32_t wanted_count = 0; + its_quadric_edge_collapse(tm.its, wanted_count, &max_error); + CHECK(!Private::exist_triangle_with_twice_vertices(tm.its.indices)); +} + +TEST_CASE("Simplified cube should not be empty.", "[its]") +{ + auto its = its_make_cube(1, 2, 3); + float max_error = std::numeric_limits::max(); + uint32_t wanted_count = 0; + its_quadric_edge_collapse(its, wanted_count, &max_error); + CHECK(!its.indices.empty()); +} diff --git a/tests/libslic3r/test_triangulation.cpp b/tests/libslic3r/test_triangulation.cpp new file mode 100644 index 0000000000..5a1f99fa97 --- /dev/null +++ b/tests/libslic3r/test_triangulation.cpp @@ -0,0 +1,127 @@ +#include + +#include +#include // only debug visualization + +using namespace Slic3r; + +namespace Private{ +void store_trinagulation(const ExPolygons &shape, + const std::vector &triangles, + const char* file_name = "C:/data/temp/triangulation.svg", + double scale = 1e5) +{ + BoundingBox bb; + for (const auto &expoly : shape) bb.merge(expoly.contour.points); + bb.scale(scale); + SVG svg_vis(file_name, bb); + svg_vis.draw(shape, "gray", .7f); + Points pts = to_points(shape); + svg_vis.draw(pts, "black", 4 * scale); + + for (const Vec3i &t : triangles) { + Slic3r::Polygon triangle({pts[t[0]], pts[t[1]], pts[t[2]]}); + triangle.scale(scale); + svg_vis.draw(triangle, "green"); + } + + // prevent visualization in test + CHECK(false); +} +} // namespace + + +TEST_CASE("Triangulate rectangle with restriction on edge", "[Triangulation]") +{ + // 0 1 2 3 + Points points = {Point(1, 1), Point(2, 1), Point(2, 2), Point(1, 2)}; + Triangulation::HalfEdges edges1 = {{1, 3}}; + std::vector indices1 = Triangulation::triangulate(points, edges1); + + auto check = [](int i1, int i2, Vec3i t) -> bool { + return true; + return (t[0] == i1 || t[1] == i1 || t[2] == i1) && + (t[0] == i2 || t[1] == i2 || t[2] == i2); + }; + REQUIRE(indices1.size() == 2); + int i1 = edges1.begin()->first, i2 = edges1.begin()->second; + CHECK(check(i1, i2, indices1[0])); + CHECK(check(i1, i2, indices1[1])); + + Triangulation::HalfEdges edges2 = {{0, 2}}; + std::vector indices2 = Triangulation::triangulate(points, edges2); + REQUIRE(indices2.size() == 2); + i1 = edges2.begin()->first; + i2 = edges2.begin()->second; + CHECK(check(i1, i2, indices2[0])); + CHECK(check(i1, i2, indices2[1])); +} + +TEST_CASE("Triangulation polygon", "[triangulation]") +{ + Points points = {Point(416, 346), Point(445, 362), Point(463, 389), + Point(469, 427), Point(445, 491)}; + + Polygon polygon(points); + Polygons polygons({polygon}); + ExPolygon expolygon(points); + ExPolygons expolygons({expolygon}); + + std::vector tp = Triangulation::triangulate(polygon); + std::vector tps = Triangulation::triangulate(polygons); + std::vector tep = Triangulation::triangulate(expolygon); + std::vector teps = Triangulation::triangulate(expolygons); + + //Private::store_trinagulation(expolygons, teps); + + CHECK(tp.size() == tps.size()); + CHECK(tep.size() == teps.size()); + CHECK(tp.size() == tep.size()); + CHECK(tp.size() == 3); +} + +TEST_CASE("Triangulation M shape polygon", "[triangulation]") +{ + // 0 1 2 3 4 + Polygon shape_M = {Point(0, 0), Point(2, 0), Point(2, 2), Point(1, 1), Point(0, 2)}; + + std::vector triangles = Triangulation::triangulate(shape_M); + + // Check outer triangle is not contain + std::set outer_triangle = {2, 3, 4}; + bool is_in = false; + for (const Vec3i &t : triangles) { + for (size_t i = 0; i < 3; i++) { + int index = t[i]; + if (outer_triangle.find(index) == outer_triangle.end()) { + is_in = false; + break; + } else { + is_in = true; + } + } + if (is_in) break; + } + + //Private::store_trinagulation({ExPolygon(shape_M)}, triangles); + CHECK(triangles.size() == 3); + CHECK(!is_in); +} + +// same point in triangulation are not Supported +TEST_CASE("Triangulation 2 polygons with same point", "[triangulation]") +{ + Slic3r::Polygon polygon1 = { + Point(416, 346), Point(445, 362), + Point(463, 389), Point(469, 427) /* This point */, + Point(445, 491) + }; + Slic3r::Polygon polygon2 = { + Point(495, 488), Point(469, 427) /* This point */, + Point(495, 364) + }; + ExPolygons shape2d = {ExPolygon(polygon1), ExPolygon(polygon2)}; + std::vector shape_triangles = Triangulation::triangulate(shape2d); + //Private::store_trinagulation(shape2d, shape_triangles); + CHECK(shape_triangles.size() == 4); +} diff --git a/tests/slic3rutils/slic3r_jobs_tests.cpp b/tests/slic3rutils/slic3r_jobs_tests.cpp index d4b912b84f..b0d01d2edd 100644 --- a/tests/slic3rutils/slic3r_jobs_tests.cpp +++ b/tests/slic3rutils/slic3r_jobs_tests.cpp @@ -118,12 +118,15 @@ TEMPLATE_LIST_TEST_CASE("cancel_all should remove all pending jobs", "[Jobs]", T }); queue_job(worker, [&jobres](Job::Ctl &) { jobres[1] = true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); }); queue_job(worker, [&jobres](Job::Ctl &) { jobres[2] = true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); }); queue_job(worker, [&jobres](Job::Ctl &) { jobres[3] = true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); }); // wait until the first job's half time to be sure, the cancel is made