@configuratorware/configurator-frontendgui 1.57.10 → 1.57.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ var _Selectors2 = require("../../../../Reducers/DesignData/Selectors");
22
22
  var _i18n = require("../../../../../Framework/i18n");
23
23
  var _Transformers = require("../../Utils/Transformers");
24
24
  var _reactRedux = require("react-redux");
25
+ var _ServiceLocator = require("../../../../ServiceLocator");
25
26
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
26
27
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
27
28
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
@@ -224,7 +225,7 @@ var DesignerSelectionToolbox = function DesignerSelectionToolbox(props) {
224
225
  toggleCustomFontOverlay(true);
225
226
  return;
226
227
  }
227
- canvas.selectedObject.fontItem = (0, _fontItemUtils.createFontItemProps)(fontItem);
228
+ _ServiceLocator.Services.designer.changeTextFontItem(canvas.selectedObject, (0, _fontItemUtils.createFontItemProps)(fontItem));
228
229
  },
229
230
  minSize: selectedObject.fontSizeMin,
230
231
  maxSize: 1000,
@@ -322,7 +323,7 @@ var DesignerSelectionToolbox = function DesignerSelectionToolbox(props) {
322
323
  isCustomFont: true
323
324
  });
324
325
  addCustomFont(fontItem);
325
- canvas.selectedObject.fontItem = (0, _fontItemUtils.createFontItemProps)(fontItem);
326
+ _ServiceLocator.Services.designer.changeTextFontItem(canvas.selectedObject, (0, _fontItemUtils.createFontItemProps)(fontItem));
326
327
  toggleCustomFontOverlay(false);
327
328
  }
328
329
  }));
@@ -1907,6 +1907,11 @@ var DesignDataService = exports["default"] = /*#__PURE__*/function () {
1907
1907
  state,
1908
1908
  showHighlight,
1909
1909
  staticBorderColor,
1910
+ dynamicDesignAreaHighlightBorderColor,
1911
+ dynamicDesignAreaHighlightBorderColorMode,
1912
+ getDesignAreaUnderlyingColor,
1913
+ allDesignAreasForBorderColor,
1914
+ borderColor,
1910
1915
  textureBuilderConfig,
1911
1916
  variantIdentifier,
1912
1917
  effectEnabled,
@@ -1919,11 +1924,16 @@ var DesignDataService = exports["default"] = /*#__PURE__*/function () {
1919
1924
  state = _ServiceLocator.Services.store.state;
1920
1925
  showHighlight = (0, _get2["default"])(state, 'designArea.showHighlight');
1921
1926
  staticBorderColor = (0, _Selectors3.getClientHighlightColor)(state);
1927
+ dynamicDesignAreaHighlightBorderColor = (0, _configuration.getConf)('visualization.dynamicDesignAreaHighlightBorderColor');
1928
+ dynamicDesignAreaHighlightBorderColorMode = (0, _configuration.getConf)('visualization.dynamicDesignAreaHighlightBorderColorMode');
1929
+ getDesignAreaUnderlyingColor = function getDesignAreaUnderlyingColor(identifier) {
1930
+ return _ServiceLocator.Services.visualization.getDesignAreaUnderlyingColor(identifier);
1931
+ }; // 2D invokes drawingFn once per area with a single-element list; unify borders from the full view.
1932
+ allDesignAreasForBorderColor = this.getDesignAreas();
1933
+ borderColor = dynamicDesignAreaHighlightBorderColor ? (0, _getDesignAreaBorderColor.createBorderColorResolver)(getDesignAreaUnderlyingColor, staticBorderColor, dynamicDesignAreaHighlightBorderColorMode, allDesignAreasForBorderColor) : staticBorderColor;
1922
1934
  textureBuilderConfig = (0, _merge["default"])({
1923
1935
  highlight: {
1924
- borderColor: (0, _configuration.getConf)('visualization.dynamicDesignAreaHighlightBorderColor') ? (0, _getDesignAreaBorderColor.createBorderColorResolver)(function (identifier) {
1925
- return _ServiceLocator.Services.visualization.getDesignAreaUnderlyingColor(identifier);
1926
- }, staticBorderColor, (0, _configuration.getConf)('visualization.dynamicDesignAreaHighlightBorderColorMode')) : staticBorderColor,
1936
+ borderColor: borderColor,
1927
1937
  backgroundColor: (0, _configuration.getConf)('visualization.designAreaHighlightBackgroundColor'),
1928
1938
  lineWidth: (0, _configuration.getConf)('visualization.designAreaHighlightBorderWidth', 2)
1929
1939
  }
@@ -2119,22 +2129,22 @@ var DesignDataService = exports["default"] = /*#__PURE__*/function () {
2119
2129
  return _ref17.apply(this, arguments);
2120
2130
  };
2121
2131
  }();
2122
- _context22.prev = 8;
2123
- _context22.next = 11;
2124
- return getDrawingContext(drawingFn, true /* update alpha */, true /* clean up texture before drawing */);
2125
- case 11:
2132
+ _context22.prev = 13;
2126
2133
  _context22.next = 16;
2134
+ return getDrawingContext(drawingFn, true /* update alpha */, true /* clean up texture before drawing */);
2135
+ case 16:
2136
+ _context22.next = 21;
2127
2137
  break;
2128
- case 13:
2129
- _context22.prev = 13;
2130
- _context22.t0 = _context22["catch"](8);
2138
+ case 18:
2139
+ _context22.prev = 18;
2140
+ _context22.t0 = _context22["catch"](13);
2131
2141
  // eslint-disable-next-line no-console
2132
2142
  console.warn('Could not get drawing context, texture update is skipped!', _context22.t0.message);
2133
- case 16:
2143
+ case 21:
2134
2144
  case "end":
2135
2145
  return _context22.stop();
2136
2146
  }
2137
- }, _callee21, null, [[8, 13]]);
2147
+ }, _callee21, this, [[13, 18]]);
2138
2148
  }));
2139
2149
  function redrawAllDesignAreas(_x20, _x21) {
2140
2150
  return _redrawAllDesignAreas.apply(this, arguments);
@@ -417,6 +417,40 @@ var DesignerService = exports["default"] = /*#__PURE__*/function () {
417
417
  text.fitCanvas();
418
418
  }
419
419
  }
420
+ }, {
421
+ key: "changeTextFontItem",
422
+ value: function changeTextFontItem(text, fontItemProps) {
423
+ if (!text || text.type !== 'Text') {
424
+ return;
425
+ }
426
+ if ((0, _configuration.getConf)('designer.autoShrinkTextOnFontChange', true)) {
427
+ this.shrinkTextAfterFontChange(text, fontItemProps);
428
+ } else {
429
+ text.fontItem = fontItemProps;
430
+ }
431
+ }
432
+ }, {
433
+ key: "shrinkTextAfterFontChange",
434
+ value: function shrinkTextAfterFontChange(text, fontItemProps) {
435
+ var wasInside = text.isFullyInsideCanvas();
436
+ var maxEffectiveFontSize = text.fontSize * text.scaling;
437
+ text.fontItem = fontItemProps;
438
+ if (!wasInside || text.locked || text.autoFit) {
439
+ return;
440
+ }
441
+ if (text.isFullyInsideCanvas()) {
442
+ return;
443
+ }
444
+ var maxScaling = maxEffectiveFontSize / text.fontSize;
445
+ text.shrinkToFitCanvas({
446
+ enlarge: false,
447
+ updatePosition: false,
448
+ maxScaling: maxScaling
449
+ });
450
+ if (typeof text.commitScalingToFontSize === 'function') {
451
+ text.commitScalingToFontSize(maxEffectiveFontSize);
452
+ }
453
+ }
420
454
  }, {
421
455
  key: "checkDesignElementRestrictions",
422
456
  value: function checkDesignElementRestrictions(targetElementType) {
@@ -3,9 +3,16 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.rgbToHsl = exports.invertColor = exports.getContrastBorderColor = exports.getComplementaryBorderColor = exports.getBorderColorForAverage = exports.createBorderColorResolver = exports.BorderColorModes = void 0;
6
+ exports.rgbToHsl = exports.invertColor = exports.getContrastBorderColor = exports.getComplementaryBorderColor = exports.getBorderColorForAverage = exports.createBorderColorResolver = exports.computeUnifiedContrastBorders = exports.BorderColorModes = void 0;
7
7
  var _styles = require("@material-ui/core/styles");
8
8
  var _Selectors = require("../Reducers/DesignArea/Selectors");
9
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
10
+ function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
11
+ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
12
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
13
+ function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
14
+ function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
15
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
9
16
  /**
10
17
  * Minimum contrast between the border and the area it is drawn on, the WCAG AA requirement for
11
18
  * graphical objects. Below it the derived color is not considered visible enough.
@@ -112,6 +119,135 @@ var getContrastBorderColor = exports.getContrastBorderColor = function getContra
112
119
  return (0, _styles.getContrastRatio)(averageHex, NEUTRAL_DARK) >= (0, _styles.getContrastRatio)(averageHex, NEUTRAL_LIGHT) ? NEUTRAL_DARK : NEUTRAL_LIGHT;
113
120
  };
114
121
 
122
+ /**
123
+ * @param {string} neutral
124
+ * @param {Array<{averageHex: string}>} areas
125
+ * @return {number}
126
+ */
127
+ var minContrastForNeutralAcrossAreas = function minContrastForNeutralAcrossAreas(neutral, areas) {
128
+ return Math.min.apply(Math, _toConsumableArray(areas.map(function (_ref3) {
129
+ var averageHex = _ref3.averageHex;
130
+ return (0, _styles.getContrastRatio)(averageHex, neutral);
131
+ })));
132
+ };
133
+
134
+ /**
135
+ * @param {string} neutral
136
+ * @param {Array<{averageHex: string}>} areas
137
+ * @return {boolean}
138
+ */
139
+ var neutralMeetsMinContrastOnAllAreas = function neutralMeetsMinContrastOnAllAreas(neutral, areas) {
140
+ return minContrastForNeutralAcrossAreas(neutral, areas) >= MIN_CONTRAST_RATIO;
141
+ };
142
+
143
+ /**
144
+ * Picks one near-black or near-white border for every design area in a highlight pass when possible,
145
+ * so similar product shades do not flip border color at the luminance tie-break.
146
+ * @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
147
+ * @param {Array<{identifier: string}>} designAreas
148
+ * @return {Map<string, string>}
149
+ */
150
+ var computeUnifiedContrastBorders = exports.computeUnifiedContrastBorders = function computeUnifiedContrastBorders(getUnderlyingColor, designAreas) {
151
+ var areas = [];
152
+ var _iterator = _createForOfIteratorHelper(designAreas),
153
+ _step;
154
+ try {
155
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
156
+ var designArea = _step.value;
157
+ var _identifier3 = designArea && designArea.identifier;
158
+ if (!_identifier3) {
159
+ continue;
160
+ }
161
+ var average = getUnderlyingColor(_identifier3);
162
+ if (!average) {
163
+ continue;
164
+ }
165
+ areas.push({
166
+ identifier: _identifier3,
167
+ averageHex: (0, _Selectors.rgbToHex)(average.r, average.g, average.b)
168
+ });
169
+ }
170
+ } catch (err) {
171
+ _iterator.e(err);
172
+ } finally {
173
+ _iterator.f();
174
+ }
175
+ var borderByIdentifier = new Map();
176
+ if (!areas.length) {
177
+ return borderByIdentifier;
178
+ }
179
+ var darkVotes = 0;
180
+ var lightVotes = 0;
181
+ for (var _i = 0, _areas = areas; _i < _areas.length; _i++) {
182
+ var averageHex = _areas[_i].averageHex;
183
+ var vote = getContrastBorderColor(averageHex);
184
+ if (vote === NEUTRAL_DARK) {
185
+ darkVotes += 1;
186
+ } else {
187
+ lightVotes += 1;
188
+ }
189
+ }
190
+ var majorityNeutral;
191
+ if (darkVotes > lightVotes) {
192
+ majorityNeutral = NEUTRAL_DARK;
193
+ } else if (lightVotes > darkVotes) {
194
+ majorityNeutral = NEUTRAL_LIGHT;
195
+ }
196
+ if (majorityNeutral && neutralMeetsMinContrastOnAllAreas(majorityNeutral, areas)) {
197
+ var _iterator2 = _createForOfIteratorHelper(areas),
198
+ _step2;
199
+ try {
200
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
201
+ var identifier = _step2.value.identifier;
202
+ borderByIdentifier.set(identifier, majorityNeutral);
203
+ }
204
+ } catch (err) {
205
+ _iterator2.e(err);
206
+ } finally {
207
+ _iterator2.f();
208
+ }
209
+ return borderByIdentifier;
210
+ }
211
+ var qualifyingNeutrals = [NEUTRAL_DARK, NEUTRAL_LIGHT].filter(function (neutral) {
212
+ return neutralMeetsMinContrastOnAllAreas(neutral, areas);
213
+ });
214
+ if (qualifyingNeutrals.length) {
215
+ var maximinNeutral = qualifyingNeutrals.reduce(function (best, neutral) {
216
+ var minContrast = minContrastForNeutralAcrossAreas(neutral, areas);
217
+ if (!best) {
218
+ return {
219
+ neutral: neutral,
220
+ minContrast: minContrast
221
+ };
222
+ }
223
+ return minContrast > best.minContrast ? {
224
+ neutral: neutral,
225
+ minContrast: minContrast
226
+ } : best;
227
+ }, null).neutral;
228
+ var _iterator3 = _createForOfIteratorHelper(areas),
229
+ _step3;
230
+ try {
231
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
232
+ var _identifier = _step3.value.identifier;
233
+ borderByIdentifier.set(_identifier, maximinNeutral);
234
+ }
235
+ } catch (err) {
236
+ _iterator3.e(err);
237
+ } finally {
238
+ _iterator3.f();
239
+ }
240
+ return borderByIdentifier;
241
+ }
242
+ for (var _i2 = 0, _areas2 = areas; _i2 < _areas2.length; _i2++) {
243
+ var _areas2$_i = _areas2[_i2],
244
+ _identifier2 = _areas2$_i.identifier,
245
+ _averageHex = _areas2$_i.averageHex;
246
+ borderByIdentifier.set(_identifier2, getContrastBorderColor(_averageHex));
247
+ }
248
+ return borderByIdentifier;
249
+ };
250
+
115
251
  /**
116
252
  * Keeps the hue of the inverted (complementary) color at a high saturation and moves the lightness
117
253
  * away from the average, only as far as needed to reach a visible contrast, so the border is a vivid
@@ -128,8 +264,8 @@ var getComplementaryBorderColor = exports.getComplementaryBorderColor = function
128
264
  saturation = _rgbToHsl.saturation;
129
265
  if (saturation >= MIN_SATURATION) {
130
266
  var lightnessCandidates = (0, _styles.getLuminance)(averageHex) > LIGHT_AVERAGE_LUMINANCE ? LIGHTNESS_ON_LIGHT_AVERAGE : LIGHTNESS_ON_DARK_AVERAGE;
131
- for (var _i = 0, _lightnessCandidates = lightnessCandidates; _i < _lightnessCandidates.length; _i++) {
132
- var lightness = _lightnessCandidates[_i];
267
+ for (var _i3 = 0, _lightnessCandidates = lightnessCandidates; _i3 < _lightnessCandidates.length; _i3++) {
268
+ var lightness = _lightnessCandidates[_i3];
133
269
  var candidate = (0, _Selectors.rgbString2hex)((0, _styles.hslToRgb)("hsl(".concat(Math.round(hue), ", ").concat(BORDER_SATURATION, "%, ").concat(lightness, "%)")));
134
270
  if (candidate && (0, _styles.getContrastRatio)(averageHex, candidate) >= MIN_CONTRAST_RATIO) {
135
271
  return candidate;
@@ -160,11 +296,31 @@ var getBorderColorForAverage = exports.getBorderColorForAverage = function getBo
160
296
  * @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
161
297
  * @param {string} fallbackColor
162
298
  * @param {string} [mode] One of BorderColorModes
299
+ * @param {Array<{identifier: string}>} [designAreas] When set in contrast mode, one border is chosen for the pass
163
300
  * @return {function(object): string}
164
301
  */
165
- var createBorderColorResolver = exports.createBorderColorResolver = function createBorderColorResolver(getUnderlyingColor, fallbackColor, mode) {
302
+ var createBorderColorResolver = exports.createBorderColorResolver = function createBorderColorResolver(getUnderlyingColor, fallbackColor, mode, designAreas) {
303
+ var useUnifiedContrast = mode !== BorderColorModes.Complementary && designAreas && designAreas.length;
304
+ var unifiedBorderByIdentifier = null;
305
+ var getUnifiedBorderByIdentifier = function getUnifiedBorderByIdentifier() {
306
+ if (!useUnifiedContrast) {
307
+ return null;
308
+ }
309
+ var sampledCount = designAreas.filter(function (designArea) {
310
+ return (designArea === null || designArea === void 0 ? void 0 : designArea.identifier) && getUnderlyingColor(designArea.identifier);
311
+ }).length;
312
+ if (!unifiedBorderByIdentifier || sampledCount > 0 && unifiedBorderByIdentifier.size < sampledCount) {
313
+ unifiedBorderByIdentifier = computeUnifiedContrastBorders(getUnderlyingColor, designAreas);
314
+ }
315
+ return unifiedBorderByIdentifier;
316
+ };
166
317
  return function (designArea) {
167
- var average = getUnderlyingColor(designArea && designArea.identifier);
318
+ var identifier = designArea && designArea.identifier;
319
+ var unified = getUnifiedBorderByIdentifier();
320
+ if (unified && identifier && unified.has(identifier)) {
321
+ return unified.get(identifier);
322
+ }
323
+ var average = getUnderlyingColor(identifier);
168
324
  return average ? getBorderColorForAverage(average, mode) : fallbackColor;
169
325
  };
170
326
  };
@@ -222,7 +222,9 @@ var applicationConfiguration = {
222
222
  // the image formats users can upload to designareas / designer items
223
223
  includeMetricContentSize: false,
224
224
  // set true to include the metric content size in the design data
225
- bulkNamesPerNameDesignFiles: false // set true to generate individual production svgs (-> production pdf in zip download) for each bulk name
225
+ bulkNamesPerNameDesignFiles: false,
226
+ // set true to generate individual production svgs (-> production pdf in zip download) for each bulk name
227
+ autoShrinkTextOnFontChange: true // shrink text to stay inside the canvas when changing to a wider/taller font
226
228
  },
227
229
 
228
230
  vectorizeImageQuality: 45000,
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@configuratorware/configurator-frontendgui",
3
- "version": "1.57.10",
3
+ "version": "1.57.12",
4
4
  "license": "UNLICENSED",
5
5
  "private": false,
6
6
  "main": "./index.js",
7
7
  "dependencies": {
8
8
  "@babel/polyfill": "^7.12.1",
9
- "@configuratorware/scripts": "1.57.10",
9
+ "@configuratorware/scripts": "1.57.12",
10
10
  "@material-ui/core": "^4.12.4",
11
11
  "@material-ui/icons": "^4.11.3",
12
12
  "@material-ui/lab": "^4.0.0-alpha.61",
@@ -36,8 +36,8 @@
36
36
  "react-router-dom": "^5.3.4",
37
37
  "react-swipeable": "^5.5.1",
38
38
  "react-zoom-pan-pinch": "^2.1.3",
39
- "redhotmagma-graphics-editor": "1.57.10",
40
- "redhotmagma-visualization": "1.57.10",
39
+ "redhotmagma-graphics-editor": "1.57.12",
40
+ "redhotmagma-visualization": "1.57.12",
41
41
  "redux": "^4.1.0",
42
42
  "redux-logger": "^3.0.6",
43
43
  "redux-persist": "^5.10.0",
@@ -64,7 +64,7 @@
64
64
  "css-loader": "^5.2.7",
65
65
  "file-loader": "^6.2.0",
66
66
  "mini-css-extract-plugin": "^2.7.6",
67
- "node-sass": "^9.0.0",
67
+ "sass": "1.32.0",
68
68
  "react": "^18.2.0",
69
69
  "react-dom": "^18.2.0",
70
70
  "react-refresh": "^0.14.0",
@@ -154,6 +154,7 @@ const externalConfiguration = {
154
154
  allowedImageUploadFormats: 'jpg,jpeg,svg,png,pdf,bmp,tif,tiff,eps,ai,gif', // the image formats users can upload to designareas / designer items
155
155
  includeMetricContentSize: false, // set true to include the metric content size in the design data
156
156
  bulkNamesPerNameDesignFiles: false, // set true to generate individual production svgs (-> production pdf in zip download) for each bulk name
157
+ // autoShrinkTextOnFontChange: true, // shrink text to stay inside the canvas when changing to a wider/taller font
157
158
  },*/
158
159
 
159
160
  //vectorizeImageQuality: 45000,
@@ -16,6 +16,7 @@ import { hasBackgroundColor } from 'App/Reducers/DesignData/Selectors';
16
16
  import { t } from 'Framework/i18n';
17
17
  import { isOriginalVector } from '../../Utils/Transformers';
18
18
  import { useSelector } from 'react-redux';
19
+ import { Services } from 'App/ServiceLocator';
19
20
 
20
21
  const styles = theme => ({
21
22
  root: {
@@ -222,8 +223,9 @@ const DesignerSelectionToolbox = props => {
222
223
  return;
223
224
  }
224
225
 
225
- canvas.selectedObject.fontItem = createFontItemProps(
226
- fontItem
226
+ Services.designer.changeTextFontItem(
227
+ canvas.selectedObject,
228
+ createFontItemProps(fontItem)
227
229
  );
228
230
  }}
229
231
  minSize={selectedObject.fontSizeMin}
@@ -366,8 +368,9 @@ const DesignerSelectionToolbox = props => {
366
368
  isCustomFont: true,
367
369
  };
368
370
  addCustomFont(fontItem);
369
- canvas.selectedObject.fontItem = createFontItemProps(
370
- fontItem
371
+ Services.designer.changeTextFontItem(
372
+ canvas.selectedObject,
373
+ createFontItemProps(fontItem)
371
374
  );
372
375
  toggleCustomFontOverlay(false);
373
376
  }}
@@ -1604,16 +1604,28 @@ export default class DesignDataService {
1604
1604
  const showHighlight = get(state, 'designArea.showHighlight');
1605
1605
 
1606
1606
  const staticBorderColor = getClientHighlightColor(state);
1607
+ const dynamicDesignAreaHighlightBorderColor = getConf(
1608
+ 'visualization.dynamicDesignAreaHighlightBorderColor'
1609
+ );
1610
+ const dynamicDesignAreaHighlightBorderColorMode = getConf(
1611
+ 'visualization.dynamicDesignAreaHighlightBorderColorMode'
1612
+ );
1613
+ const getDesignAreaUnderlyingColor = identifier =>
1614
+ Services.visualization.getDesignAreaUnderlyingColor(identifier);
1615
+ // 2D invokes drawingFn once per area with a single-element list; unify borders from the full view.
1616
+ const allDesignAreasForBorderColor = this.getDesignAreas();
1617
+ const borderColor = dynamicDesignAreaHighlightBorderColor
1618
+ ? createBorderColorResolver(
1619
+ getDesignAreaUnderlyingColor,
1620
+ staticBorderColor,
1621
+ dynamicDesignAreaHighlightBorderColorMode,
1622
+ allDesignAreasForBorderColor
1623
+ )
1624
+ : staticBorderColor;
1607
1625
  const textureBuilderConfig = merge(
1608
1626
  {
1609
1627
  highlight: {
1610
- borderColor: getConf('visualization.dynamicDesignAreaHighlightBorderColor')
1611
- ? createBorderColorResolver(
1612
- identifier => Services.visualization.getDesignAreaUnderlyingColor(identifier),
1613
- staticBorderColor,
1614
- getConf('visualization.dynamicDesignAreaHighlightBorderColorMode')
1615
- )
1616
- : staticBorderColor,
1628
+ borderColor,
1617
1629
  backgroundColor: getConf('visualization.designAreaHighlightBackgroundColor'),
1618
1630
  lineWidth: getConf('visualization.designAreaHighlightBorderWidth', 2),
1619
1631
  },
@@ -267,6 +267,45 @@ export default class DesignerService {
267
267
  }
268
268
  }
269
269
 
270
+ changeTextFontItem(text, fontItemProps) {
271
+ if (!text || text.type !== 'Text') {
272
+ return;
273
+ }
274
+
275
+ if (getConf('designer.autoShrinkTextOnFontChange', true)) {
276
+ this.shrinkTextAfterFontChange(text, fontItemProps);
277
+ } else {
278
+ text.fontItem = fontItemProps;
279
+ }
280
+ }
281
+
282
+ shrinkTextAfterFontChange(text, fontItemProps) {
283
+ const wasInside = text.isFullyInsideCanvas();
284
+ const maxEffectiveFontSize = text.fontSize * text.scaling;
285
+
286
+ text.fontItem = fontItemProps;
287
+
288
+ if (!wasInside || text.locked || text.autoFit) {
289
+ return;
290
+ }
291
+
292
+ if (text.isFullyInsideCanvas()) {
293
+ return;
294
+ }
295
+
296
+ const maxScaling = maxEffectiveFontSize / text.fontSize;
297
+
298
+ text.shrinkToFitCanvas({
299
+ enlarge: false,
300
+ updatePosition: false,
301
+ maxScaling,
302
+ });
303
+
304
+ if (typeof text.commitScalingToFontSize === 'function') {
305
+ text.commitScalingToFontSize(maxEffectiveFontSize);
306
+ }
307
+ }
308
+
270
309
  checkDesignElementRestrictions(targetElementType) {
271
310
  const state = Services.store.state;
272
311
  const designData = getDesignDataForSelectedDesignArea(state);
@@ -2,6 +2,11 @@ import DesignerService from '../DesignerService';
2
2
  import ServiceLocator from '../../ServiceLocator';
3
3
  import { SET_SELECTED_IMAGE } from '../../Reducers/ImageGallery/Actions';
4
4
  import DesignDataService from '../DesignDataService';
5
+ import { getConf } from '../../configuration';
6
+
7
+ jest.mock('../../configuration', () => ({
8
+ getConf: jest.fn((key, defaultValue) => defaultValue),
9
+ }));
5
10
 
6
11
  describe('Services/DesignerService', () => {
7
12
  it('openImageEditDialog selects the edited image and loads the tags and gallery images', async () => {
@@ -170,4 +175,102 @@ describe('Services/DesignerService', () => {
170
175
  `"<span data-placeholder=\\"true\\"><span style='color: #f73429; font-family: \\"Lobster\\"; font-weight: normal; font-style: normal;' data-color='{\\"color\\":\\"red\\",\\"palette\\":\\"default\\"}'>sample text to be replaced</span></span>"`
171
176
  );
172
177
  });
178
+
179
+ describe('changeTextFontItem', () => {
180
+ const createTextMock = (overrides = {}) => ({
181
+ type: 'Text',
182
+ fontSize: 20,
183
+ scaling: 1,
184
+ x: 30,
185
+ y: 40,
186
+ locked: false,
187
+ autoFit: false,
188
+ fontItem: {},
189
+ isFullyInsideCanvas: jest.fn(() => true),
190
+ shrinkToFitCanvas: jest.fn(),
191
+ commitScalingToFontSize: jest.fn(),
192
+ ...overrides,
193
+ });
194
+
195
+ beforeEach(() => {
196
+ getConf.mockImplementation((key, defaultValue) => defaultValue);
197
+ });
198
+
199
+ it('applies font item and shrinks when text was inside but overflows after font change', () => {
200
+ const service = new DesignerService();
201
+ const text = createTextMock({
202
+ isFullyInsideCanvas: jest
203
+ .fn()
204
+ .mockReturnValueOnce(true)
205
+ .mockReturnValueOnce(false),
206
+ });
207
+ const fontItemProps = { family: 'Arial' };
208
+
209
+ service.changeTextFontItem(text, fontItemProps);
210
+
211
+ expect(text.fontItem).toBe(fontItemProps);
212
+ expect(text.isFullyInsideCanvas).toHaveBeenCalledTimes(2);
213
+ expect(text.shrinkToFitCanvas).toHaveBeenCalledWith({
214
+ enlarge: false,
215
+ updatePosition: false,
216
+ maxScaling: 1,
217
+ });
218
+ expect(text.commitScalingToFontSize).toHaveBeenCalledWith(20);
219
+ });
220
+
221
+ it('does not shrink when text still fits inside the canvas after font change', () => {
222
+ const service = new DesignerService();
223
+ const text = createTextMock({
224
+ isFullyInsideCanvas: jest.fn(() => true),
225
+ });
226
+
227
+ service.changeTextFontItem(text, { family: 'Arial' });
228
+
229
+ expect(text.fontItem).toEqual({ family: 'Arial' });
230
+ expect(text.isFullyInsideCanvas).toHaveBeenCalledTimes(2);
231
+ expect(text.shrinkToFitCanvas).not.toHaveBeenCalled();
232
+ expect(text.commitScalingToFontSize).not.toHaveBeenCalled();
233
+ });
234
+
235
+ it('does not shrink when text was not fully inside the canvas', () => {
236
+ const service = new DesignerService();
237
+ const text = createTextMock({
238
+ isFullyInsideCanvas: jest.fn(() => false),
239
+ });
240
+
241
+ service.changeTextFontItem(text, { family: 'Arial' });
242
+
243
+ expect(text.shrinkToFitCanvas).not.toHaveBeenCalled();
244
+ });
245
+
246
+ it('does not shrink locked or autoFit text', () => {
247
+ const service = new DesignerService();
248
+ const fontItemProps = { family: 'Arial' };
249
+ const lockedText = createTextMock({ locked: true });
250
+ service.shrinkTextAfterFontChange(lockedText, fontItemProps);
251
+ expect(lockedText.fontItem).toBe(fontItemProps);
252
+ expect(lockedText.shrinkToFitCanvas).not.toHaveBeenCalled();
253
+
254
+ const autoFitText = createTextMock({ autoFit: true });
255
+ service.shrinkTextAfterFontChange(autoFitText, fontItemProps);
256
+ expect(autoFitText.fontItem).toBe(fontItemProps);
257
+ expect(autoFitText.shrinkToFitCanvas).not.toHaveBeenCalled();
258
+ });
259
+
260
+ it('skips shrink when autoShrinkTextOnFontChange is disabled', () => {
261
+ getConf.mockImplementation((key, defaultValue) => {
262
+ if (key === 'designer.autoShrinkTextOnFontChange') {
263
+ return false;
264
+ }
265
+ return defaultValue;
266
+ });
267
+ const service = new DesignerService();
268
+ const text = createTextMock();
269
+
270
+ service.changeTextFontItem(text, { family: 'Arial' });
271
+
272
+ expect(text.isFullyInsideCanvas).not.toHaveBeenCalled();
273
+ expect(text.shrinkToFitCanvas).not.toHaveBeenCalled();
274
+ });
275
+ });
173
276
  });
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  BorderColorModes,
3
+ computeUnifiedContrastBorders,
3
4
  createBorderColorResolver,
4
5
  getBorderColorForAverage,
6
+ getContrastBorderColor,
5
7
  invertColor,
6
8
  rgbToHsl,
7
9
  } from '../getDesignAreaBorderColor';
@@ -96,6 +98,70 @@ describe('getBorderColorForAverage in complementary mode', () => {
96
98
  });
97
99
  });
98
100
 
101
+ const grey = value => ({ r: value, g: value, b: value });
102
+
103
+ describe('computeUnifiedContrastBorders', () => {
104
+ test('uses one border for similar greys that would flip individually', () => {
105
+ const colors = {
106
+ left: grey(100),
107
+ right: grey(120),
108
+ };
109
+ const borders = computeUnifiedContrastBorders(id => colors[id], [
110
+ { identifier: 'left' },
111
+ { identifier: 'right' },
112
+ ]);
113
+
114
+ expect(getContrastBorderColor('#646464')).toBe('#eeeeee');
115
+ expect(getContrastBorderColor('#787878')).toBe('#111111');
116
+ expect(borders.get('left')).toBe(borders.get('right'));
117
+ });
118
+
119
+ test('uses majority neutral when it is visible on every area', () => {
120
+ const colors = {
121
+ a: grey(50),
122
+ b: grey(60),
123
+ c: grey(128),
124
+ };
125
+ const borders = computeUnifiedContrastBorders(id => colors[id], [
126
+ { identifier: 'a' },
127
+ { identifier: 'b' },
128
+ { identifier: 'c' },
129
+ ]);
130
+
131
+ expect(borders.get('a')).toBe('#eeeeee');
132
+ expect(borders.get('b')).toBe('#eeeeee');
133
+ expect(borders.get('c')).toBe('#eeeeee');
134
+ });
135
+
136
+ test('falls back to maximin when majority neutral fails on an area', () => {
137
+ const colors = {
138
+ dark: grey(30),
139
+ mid: grey(128),
140
+ };
141
+ const borders = computeUnifiedContrastBorders(id => colors[id], [
142
+ { identifier: 'dark' },
143
+ { identifier: 'mid' },
144
+ ]);
145
+
146
+ expect(borders.get('dark')).toBe('#eeeeee');
147
+ expect(borders.get('mid')).toBe('#eeeeee');
148
+ });
149
+
150
+ test('allows per-area colors when no single neutral works everywhere', () => {
151
+ const colors = {
152
+ light: { r: 255, g: 255, b: 255 },
153
+ dark: { r: 0, g: 0, b: 0 },
154
+ };
155
+ const borders = computeUnifiedContrastBorders(id => colors[id], [
156
+ { identifier: 'light' },
157
+ { identifier: 'dark' },
158
+ ]);
159
+
160
+ expect(borders.get('light')).toBe('#111111');
161
+ expect(borders.get('dark')).toBe('#eeeeee');
162
+ });
163
+ });
164
+
99
165
  describe('createBorderColorResolver', () => {
100
166
  test('resolves the border color from the underlying color of the design area', () => {
101
167
  const getUnderlyingColor = jest.fn().mockReturnValue(JACKET_GREEN);
@@ -125,4 +191,75 @@ describe('createBorderColorResolver', () => {
125
191
  test('falls back to the given color without a design area', () => {
126
192
  expect(createBorderColorResolver(() => null, '#abcdef')()).toBe('#abcdef');
127
193
  });
194
+
195
+ test('unifies contrast borders across design areas when the list is given', () => {
196
+ const colors = {
197
+ left: grey(100),
198
+ right: grey(120),
199
+ };
200
+ const resolve = createBorderColorResolver(id => colors[id], '#abcdef', BorderColorModes.Contrast, [
201
+ { identifier: 'left' },
202
+ { identifier: 'right' },
203
+ ]);
204
+
205
+ expect(resolve({ identifier: 'left' })).toBe(resolve({ identifier: 'right' }));
206
+ });
207
+
208
+ test('unifies when resolved one area at a time like 2D drawing passes', () => {
209
+ const colors = {
210
+ chestLeft: grey(100),
211
+ chestRight: grey(120),
212
+ collar: grey(115),
213
+ };
214
+ const allAreas = [
215
+ { identifier: 'chestLeft' },
216
+ { identifier: 'chestRight' },
217
+ { identifier: 'collar' },
218
+ ];
219
+ const resolve = createBorderColorResolver(
220
+ id => colors[id],
221
+ '#abcdef',
222
+ BorderColorModes.Contrast,
223
+ allAreas
224
+ );
225
+
226
+ const chestLeftBorder = resolve({ identifier: 'chestLeft' });
227
+ const chestRightBorder = resolve({ identifier: 'chestRight' });
228
+ const collarBorder = resolve({ identifier: 'collar' });
229
+
230
+ expect(chestLeftBorder).toBe(chestRightBorder);
231
+ expect(chestRightBorder).toBe(collarBorder);
232
+ });
233
+
234
+ test('resolves per area when design areas are omitted', () => {
235
+ const resolve = createBorderColorResolver(
236
+ id => (id === 'left' ? grey(100) : grey(120)),
237
+ '#abcdef',
238
+ BorderColorModes.Contrast
239
+ );
240
+
241
+ expect(resolve({ identifier: 'left' })).toBe('#eeeeee');
242
+ expect(resolve({ identifier: 'right' })).toBe('#111111');
243
+ });
244
+
245
+ test('does not unify borders in complementary mode', () => {
246
+ const colors = {
247
+ left: grey(100),
248
+ right: grey(120),
249
+ };
250
+ const resolve = createBorderColorResolver(
251
+ id => colors[id],
252
+ '#abcdef',
253
+ BorderColorModes.Complementary,
254
+ [{ identifier: 'left' }, { identifier: 'right' }]
255
+ );
256
+
257
+ expect(resolve({ identifier: 'left' })).toBe(
258
+ getBorderColorForAverage(colors.left, BorderColorModes.Complementary)
259
+ );
260
+ expect(resolve({ identifier: 'right' })).toBe(
261
+ getBorderColorForAverage(colors.right, BorderColorModes.Complementary)
262
+ );
263
+ expect(resolve({ identifier: 'left' })).not.toBe(resolve({ identifier: 'right' }));
264
+ });
128
265
  });
@@ -95,6 +95,105 @@ export const getContrastBorderColor = averageHex =>
95
95
  ? NEUTRAL_DARK
96
96
  : NEUTRAL_LIGHT;
97
97
 
98
+ /**
99
+ * @param {string} neutral
100
+ * @param {Array<{averageHex: string}>} areas
101
+ * @return {number}
102
+ */
103
+ const minContrastForNeutralAcrossAreas = (neutral, areas) =>
104
+ Math.min(...areas.map(({ averageHex }) => getContrastRatio(averageHex, neutral)));
105
+
106
+ /**
107
+ * @param {string} neutral
108
+ * @param {Array<{averageHex: string}>} areas
109
+ * @return {boolean}
110
+ */
111
+ const neutralMeetsMinContrastOnAllAreas = (neutral, areas) =>
112
+ minContrastForNeutralAcrossAreas(neutral, areas) >= MIN_CONTRAST_RATIO;
113
+
114
+ /**
115
+ * Picks one near-black or near-white border for every design area in a highlight pass when possible,
116
+ * so similar product shades do not flip border color at the luminance tie-break.
117
+ * @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
118
+ * @param {Array<{identifier: string}>} designAreas
119
+ * @return {Map<string, string>}
120
+ */
121
+ export const computeUnifiedContrastBorders = (getUnderlyingColor, designAreas) => {
122
+ const areas = [];
123
+
124
+ for (const designArea of designAreas) {
125
+ const identifier = designArea && designArea.identifier;
126
+ if (!identifier) {
127
+ continue;
128
+ }
129
+ const average = getUnderlyingColor(identifier);
130
+ if (!average) {
131
+ continue;
132
+ }
133
+ areas.push({
134
+ identifier,
135
+ averageHex: rgbToHex(average.r, average.g, average.b),
136
+ });
137
+ }
138
+
139
+ const borderByIdentifier = new Map();
140
+
141
+ if (!areas.length) {
142
+ return borderByIdentifier;
143
+ }
144
+
145
+ let darkVotes = 0;
146
+ let lightVotes = 0;
147
+
148
+ for (const { averageHex } of areas) {
149
+ const vote = getContrastBorderColor(averageHex);
150
+ if (vote === NEUTRAL_DARK) {
151
+ darkVotes += 1;
152
+ } else {
153
+ lightVotes += 1;
154
+ }
155
+ }
156
+
157
+ let majorityNeutral;
158
+ if (darkVotes > lightVotes) {
159
+ majorityNeutral = NEUTRAL_DARK;
160
+ } else if (lightVotes > darkVotes) {
161
+ majorityNeutral = NEUTRAL_LIGHT;
162
+ }
163
+
164
+ if (majorityNeutral && neutralMeetsMinContrastOnAllAreas(majorityNeutral, areas)) {
165
+ for (const { identifier } of areas) {
166
+ borderByIdentifier.set(identifier, majorityNeutral);
167
+ }
168
+ return borderByIdentifier;
169
+ }
170
+
171
+ const qualifyingNeutrals = [NEUTRAL_DARK, NEUTRAL_LIGHT].filter(neutral =>
172
+ neutralMeetsMinContrastOnAllAreas(neutral, areas)
173
+ );
174
+
175
+ if (qualifyingNeutrals.length) {
176
+ const maximinNeutral = qualifyingNeutrals.reduce((best, neutral) => {
177
+ const minContrast = minContrastForNeutralAcrossAreas(neutral, areas);
178
+ if (!best) {
179
+ return { neutral, minContrast };
180
+ }
181
+ return minContrast > best.minContrast ? { neutral, minContrast } : best;
182
+ }, null).neutral;
183
+
184
+ for (const { identifier } of areas) {
185
+ borderByIdentifier.set(identifier, maximinNeutral);
186
+ }
187
+ return borderByIdentifier;
188
+ }
189
+
190
+ for (const { identifier, averageHex } of areas) {
191
+ borderByIdentifier.set(identifier, getContrastBorderColor(averageHex));
192
+ }
193
+
194
+ return borderByIdentifier;
195
+ };
196
+
98
197
  /**
99
198
  * Keeps the hue of the inverted (complementary) color at a high saturation and moves the lightness
100
199
  * away from the average, only as far as needed to reach a visible contrast, so the border is a vivid
@@ -150,9 +249,38 @@ export const getBorderColorForAverage = (average, mode = BorderColorModes.Contra
150
249
  * @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
151
250
  * @param {string} fallbackColor
152
251
  * @param {string} [mode] One of BorderColorModes
252
+ * @param {Array<{identifier: string}>} [designAreas] When set in contrast mode, one border is chosen for the pass
153
253
  * @return {function(object): string}
154
254
  */
155
- export const createBorderColorResolver = (getUnderlyingColor, fallbackColor, mode) => designArea => {
156
- const average = getUnderlyingColor(designArea && designArea.identifier);
157
- return average ? getBorderColorForAverage(average, mode) : fallbackColor;
255
+ export const createBorderColorResolver = (getUnderlyingColor, fallbackColor, mode, designAreas) => {
256
+ const useUnifiedContrast = mode !== BorderColorModes.Complementary && designAreas && designAreas.length;
257
+ let unifiedBorderByIdentifier = null;
258
+
259
+ const getUnifiedBorderByIdentifier = () => {
260
+ if (!useUnifiedContrast) {
261
+ return null;
262
+ }
263
+ const sampledCount = designAreas.filter(
264
+ designArea => designArea?.identifier && getUnderlyingColor(designArea.identifier)
265
+ ).length;
266
+ if (
267
+ !unifiedBorderByIdentifier ||
268
+ (sampledCount > 0 && unifiedBorderByIdentifier.size < sampledCount)
269
+ ) {
270
+ unifiedBorderByIdentifier = computeUnifiedContrastBorders(getUnderlyingColor, designAreas);
271
+ }
272
+ return unifiedBorderByIdentifier;
273
+ };
274
+
275
+ return designArea => {
276
+ const identifier = designArea && designArea.identifier;
277
+ const unified = getUnifiedBorderByIdentifier();
278
+
279
+ if (unified && identifier && unified.has(identifier)) {
280
+ return unified.get(identifier);
281
+ }
282
+
283
+ const average = getUnderlyingColor(identifier);
284
+ return average ? getBorderColorForAverage(average, mode) : fallbackColor;
285
+ };
158
286
  };
@@ -229,6 +229,7 @@ let applicationConfiguration = {
229
229
  allowedImageUploadFormats: 'jpg,jpeg,svg,png,pdf,bmp,tif,tiff,eps,ai,gif', // the image formats users can upload to designareas / designer items
230
230
  includeMetricContentSize: false, // set true to include the metric content size in the design data
231
231
  bulkNamesPerNameDesignFiles: false, // set true to generate individual production svgs (-> production pdf in zip download) for each bulk name
232
+ autoShrinkTextOnFontChange: true, // shrink text to stay inside the canvas when changing to a wider/taller font
232
233
  },
233
234
 
234
235
  vectorizeImageQuality: 45000,