redhotmagma-visualization 1.57.6 → 1.57.8
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.
- package/Three/Three2D.js +135 -30
- package/Utils/DesignArea/getUvBoundaryPaths.js +125 -0
- package/Utils/DesignArea/sampleBorderRingColor.js +251 -0
- package/Utils/TextureBuilder/createTextureBuilder.js +31 -5
- package/package.json +2 -2
- package/src/Three/Three2D.js +103 -0
- package/src/Utils/DesignArea/__tests__/getUvBoundaryPaths.test.js +191 -0
- package/src/Utils/DesignArea/__tests__/sampleBorderRingColor.test.js +229 -0
- package/src/Utils/DesignArea/getUvBoundaryPaths.js +110 -0
- package/src/Utils/DesignArea/sampleBorderRingColor.js +200 -0
- package/src/Utils/TextureBuilder/__tests__/createTextureBuilder.test.js +104 -0
- package/src/Utils/TextureBuilder/createTextureBuilder.js +24 -3
package/Three/Three2D.js
CHANGED
|
@@ -34,7 +34,7 @@ function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? O
|
|
|
34
34
|
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
35
35
|
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
|
|
36
36
|
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
37
|
-
import { BackSide, Box3, CylinderGeometry, Group, ImageLoader, Mesh, MeshBasicMaterial, NearestFilter, LinearFilter, OrthographicCamera, PlaneGeometry, Texture, Vector2, Vector3, WebGLRenderer, BufferGeometry, Float32BufferAttribute, DoubleSide, LinearSRGBColorSpace } from "three";
|
|
37
|
+
import { BackSide, Box3, CylinderGeometry, Group, ImageLoader, Matrix3, Mesh, MeshBasicMaterial, NearestFilter, LinearFilter, OrthographicCamera, PlaneGeometry, Texture, Vector2, Vector3, WebGLRenderer, BufferGeometry, Float32BufferAttribute, DoubleSide, LinearSRGBColorSpace } from "three";
|
|
38
38
|
import TWEEN from "@tweenjs/tween.js";
|
|
39
39
|
import get from "lodash/get";
|
|
40
40
|
import first from "lodash/first";
|
|
@@ -50,6 +50,8 @@ import { createMouseWheelEventListener, createPointerMoveEventListener } from ".
|
|
|
50
50
|
import { EventTypes, Numeric } from "../Constants";
|
|
51
51
|
import { createTextureBuilder, optimizeSizeToTexture } from "../Utils/TextureBuilder/createTextureBuilder";
|
|
52
52
|
import { getDesignAreaCanvasSize } from "../Utils/DesignArea/getDesignAreaCanvasSize";
|
|
53
|
+
import { sampleRingAverage } from "../Utils/DesignArea/sampleBorderRingColor";
|
|
54
|
+
import { buildVisiblePaths, getUvBoundaryIndices } from "../Utils/DesignArea/getUvBoundaryPaths";
|
|
53
55
|
import { disposeMaterial } from "../Utils/disposeMaterial";
|
|
54
56
|
import { createImageRepository } from "../Utils/createImageRepository";
|
|
55
57
|
import { createCache } from "../Utils/createCache";
|
|
@@ -76,6 +78,7 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
76
78
|
_classCallCheck(this, Three2D);
|
|
77
79
|
_this = _super.call(this, cfg);
|
|
78
80
|
_defineProperty(_assertThisInitialized(_this), "_materialMap", new Map());
|
|
81
|
+
_defineProperty(_assertThisInitialized(_this), "_underlyingColorCache", new Map());
|
|
79
82
|
_defineProperty(_assertThisInitialized(_this), "_designAreaList", []);
|
|
80
83
|
_defineProperty(_assertThisInitialized(_this), "_currentDesignView", null);
|
|
81
84
|
_defineProperty(_assertThisInitialized(_this), "_currentDesignArea", null);
|
|
@@ -808,6 +811,105 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
808
811
|
width: width,
|
|
809
812
|
height: height
|
|
810
813
|
};
|
|
814
|
+
// the sampled colors belong to the previous product image
|
|
815
|
+
this._underlyingColorCache.clear();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Reads the average color of the product image underneath the border of the given design area.
|
|
820
|
+
* It is used to derive a border color that stays visible on the current product image.
|
|
821
|
+
* Returns null when it cannot be determined, so the caller can fall back to a static color.
|
|
822
|
+
* @param {string} designAreaIdentifier
|
|
823
|
+
* @returns {{r: number, g: number, b: number}|null}
|
|
824
|
+
*/
|
|
825
|
+
}, {
|
|
826
|
+
key: "getDesignAreaUnderlyingColor",
|
|
827
|
+
value: function getDesignAreaUnderlyingColor(designAreaIdentifier) {
|
|
828
|
+
var _this$_model2;
|
|
829
|
+
var image = (_this$_model2 = this._model) === null || _this$_model2 === void 0 || (_this$_model2 = _this$_model2.material) === null || _this$_model2 === void 0 || (_this$_model2 = _this$_model2.map) === null || _this$_model2 === void 0 ? void 0 : _this$_model2.image;
|
|
830
|
+
if (!image || !this._size.width || !this._size.height) {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
// getModelPath() covers the view and the material variant, so every product image
|
|
835
|
+
// that can be shown gets its own entry
|
|
836
|
+
var cacheKey = "".concat(this.getModelPath(), "|").concat(designAreaIdentifier);
|
|
837
|
+
if (this._underlyingColorCache.has(cacheKey)) {
|
|
838
|
+
return this._underlyingColorCache.get(cacheKey);
|
|
839
|
+
}
|
|
840
|
+
var color = this._sampleDesignAreaUnderlyingColor(designAreaIdentifier, image);
|
|
841
|
+
this._underlyingColorCache.set(cacheKey, color);
|
|
842
|
+
return color;
|
|
843
|
+
} catch (e) {
|
|
844
|
+
// this runs while drawing, so never let it break the render
|
|
845
|
+
// eslint-disable-next-line no-console
|
|
846
|
+
console.warn("Could not determine the color underneath the design area border.", e);
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}, {
|
|
851
|
+
key: "_sampleDesignAreaUnderlyingColor",
|
|
852
|
+
value: function _sampleDesignAreaUnderlyingColor(designAreaIdentifier, image) {
|
|
853
|
+
var _geometry$attributes,
|
|
854
|
+
_this7 = this;
|
|
855
|
+
var mesh = this.findAreaObjectByName(designAreaIdentifier);
|
|
856
|
+
var geometry = mesh === null || mesh === void 0 ? void 0 : mesh.geometry;
|
|
857
|
+
if (!(geometry !== null && geometry !== void 0 && (_geometry$attributes = geometry.attributes) !== null && _geometry$attributes !== void 0 && _geometry$attributes.position)) {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
var indices = getUvBoundaryIndices(geometry);
|
|
861
|
+
if (!indices.length) {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
mesh.updateWorldMatrix(true, false);
|
|
865
|
+
var position = geometry.attributes.position;
|
|
866
|
+
var normal = geometry.attributes.normal;
|
|
867
|
+
|
|
868
|
+
// with optimizeProductImage the texture holds a resized copy, while the scene still uses the
|
|
869
|
+
// original image size, so the mesh has to be projected into the pixel space of the source
|
|
870
|
+
var scaling = image.width / this._size.width;
|
|
871
|
+
var point = new Vector3();
|
|
872
|
+
var project = function project(index) {
|
|
873
|
+
point.fromBufferAttribute(position, index).applyMatrix4(mesh.matrixWorld);
|
|
874
|
+
return {
|
|
875
|
+
// the product plane is centered in the scene and three.js has the y axis pointing up,
|
|
876
|
+
// while the image pixels are counted downwards from the top left corner
|
|
877
|
+
x: (_this7._size.width / 2 + point.x) * scaling,
|
|
878
|
+
y: (_this7._size.height / 2 - point.y) * scaling
|
|
879
|
+
};
|
|
880
|
+
};
|
|
881
|
+
var paths = buildVisiblePaths(indices, this._createVisibilityCheck(mesh, normal), project);
|
|
882
|
+
return sampleRingAverage(image, paths);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Back facing parts of a design area are culled by the renderer, so the border is not drawn there
|
|
887
|
+
* and must not be sampled either. A rotated cylinder that spans a wide angle turns part of its
|
|
888
|
+
* own border away from the camera.
|
|
889
|
+
* @param {import("three").Mesh} mesh
|
|
890
|
+
* @param {import("three").BufferAttribute} normal
|
|
891
|
+
* @returns {function(number): boolean}
|
|
892
|
+
*/
|
|
893
|
+
}, {
|
|
894
|
+
key: "_createVisibilityCheck",
|
|
895
|
+
value: function _createVisibilityCheck(mesh, normal) {
|
|
896
|
+
var _mesh$material, _mesh$material2;
|
|
897
|
+
// planes are rendered with DoubleSide, so both of their faces are visible
|
|
898
|
+
if (!normal || ((_mesh$material = mesh.material) === null || _mesh$material === void 0 ? void 0 : _mesh$material.side) === DoubleSide) {
|
|
899
|
+
return function () {
|
|
900
|
+
return true;
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// the camera looks along -z, so a face pointing towards +z faces the camera,
|
|
905
|
+
// unless the material was flipped to the back side by the "inside" flag
|
|
906
|
+
var towardsCamera = ((_mesh$material2 = mesh.material) === null || _mesh$material2 === void 0 ? void 0 : _mesh$material2.side) === BackSide ? -1 : 1;
|
|
907
|
+
var normalMatrix = new Matrix3().getNormalMatrix(mesh.matrixWorld);
|
|
908
|
+
var direction = new Vector3();
|
|
909
|
+
return function (index) {
|
|
910
|
+
direction.fromBufferAttribute(normal, index).applyMatrix3(normalMatrix);
|
|
911
|
+
return direction.z * towardsCamera > 0;
|
|
912
|
+
};
|
|
811
913
|
}
|
|
812
914
|
}, {
|
|
813
915
|
key: "getOffset",
|
|
@@ -866,7 +968,7 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
866
968
|
}, {
|
|
867
969
|
key: "moveCamera",
|
|
868
970
|
value: function moveCamera(position, zoom) {
|
|
869
|
-
var
|
|
971
|
+
var _this8 = this;
|
|
870
972
|
var duration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
871
973
|
var viewOffset = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
|
|
872
974
|
this.cleanUpCameraAnimations();
|
|
@@ -875,7 +977,7 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
875
977
|
new TWEEN.Tween(this.camera, this.cameraTweenGroup).to({
|
|
876
978
|
zoom: zoom
|
|
877
979
|
}, duration.zoom || 1000).easing(TWEEN.Easing.Quadratic.Out).onUpdate(function () {
|
|
878
|
-
return
|
|
980
|
+
return _this8.camera.updateProjectionMatrix();
|
|
879
981
|
}).onComplete(animationDef.createResolver()).start();
|
|
880
982
|
}
|
|
881
983
|
if (position) {
|
|
@@ -892,7 +994,7 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
892
994
|
new TWEEN.Tween(currentViewOffset, this.cameraTweenGroup).to(viewOffset, duration.viewOffset || 1000).easing(TWEEN.Easing.Quadratic.Out).onComplete(animationDef.createResolver()).start();
|
|
893
995
|
}
|
|
894
996
|
animationDef.then(function () {
|
|
895
|
-
return
|
|
997
|
+
return _this8.stopAnimation("camera");
|
|
896
998
|
});
|
|
897
999
|
this.startAnimation("camera");
|
|
898
1000
|
}
|
|
@@ -921,8 +1023,11 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
921
1023
|
}, {
|
|
922
1024
|
key: "createObjectsForDesignAreas",
|
|
923
1025
|
value: function createObjectsForDesignAreas(designAreas) {
|
|
924
|
-
var
|
|
1026
|
+
var _this9 = this;
|
|
925
1027
|
var containerGroup = new Group();
|
|
1028
|
+
|
|
1029
|
+
// the design area geometry is rebuilt here, so previously sampled colors may be outdated
|
|
1030
|
+
this._underlyingColorCache.clear();
|
|
926
1031
|
var EPS = 10;
|
|
927
1032
|
var index = 1;
|
|
928
1033
|
designAreas.forEach(function (designArea) {
|
|
@@ -931,7 +1036,7 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
931
1036
|
try {
|
|
932
1037
|
geometry = createBaseShapeGeometry(designArea);
|
|
933
1038
|
} catch (e) {
|
|
934
|
-
|
|
1039
|
+
_this9.observable.dispatch(EventTypes.Error, e);
|
|
935
1040
|
throw e;
|
|
936
1041
|
}
|
|
937
1042
|
var _x$y$z$rotation$baseS = _objectSpread({
|
|
@@ -948,7 +1053,7 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
948
1053
|
y = _x$y$z$rotation$baseS.y,
|
|
949
1054
|
z = _x$y$z$rotation$baseS.z,
|
|
950
1055
|
rotation = _x$y$z$rotation$baseS.rotation;
|
|
951
|
-
var mat =
|
|
1056
|
+
var mat = _this9.createCanvasMaterial(designArea);
|
|
952
1057
|
var obj = new Mesh(geometry, mat);
|
|
953
1058
|
Object.assign(obj.rotation, rotation);
|
|
954
1059
|
obj.position.x = x;
|
|
@@ -1227,7 +1332,7 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
1227
1332
|
key: "getScreenshots",
|
|
1228
1333
|
value: function () {
|
|
1229
1334
|
var _getScreenshots = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee14(callback) {
|
|
1230
|
-
var
|
|
1335
|
+
var _this0 = this;
|
|
1231
1336
|
var _ref0,
|
|
1232
1337
|
designViewList,
|
|
1233
1338
|
areaList,
|
|
@@ -1283,16 +1388,16 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
1283
1388
|
return _regeneratorRuntime().wrap(function _callee12$(_context12) {
|
|
1284
1389
|
while (1) switch (_context12.prev = _context12.next) {
|
|
1285
1390
|
case 0:
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1391
|
+
_this0._screenshotCamera = null;
|
|
1392
|
+
_this0._screenshottingInProgress = false;
|
|
1393
|
+
_this0._materialVariantPath = origVariantPath;
|
|
1394
|
+
_this0._currentDesignView = null;
|
|
1290
1395
|
_context12.next = 6;
|
|
1291
|
-
return
|
|
1396
|
+
return _this0.setView(origDesignView);
|
|
1292
1397
|
case 6:
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1398
|
+
_this0.renderer.setSize(origSize.width, origSize.height);
|
|
1399
|
+
_this0.renderer.setPixelRatio(origPixelRatio);
|
|
1400
|
+
_this0.render();
|
|
1296
1401
|
removeTempOverlay();
|
|
1297
1402
|
case 10:
|
|
1298
1403
|
case "end":
|
|
@@ -1319,8 +1424,8 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
1319
1424
|
case 0:
|
|
1320
1425
|
variant = _step.value;
|
|
1321
1426
|
data = {};
|
|
1322
|
-
|
|
1323
|
-
|
|
1427
|
+
_this0.setMaterialVariantPath(variant);
|
|
1428
|
+
_this0._currentDesignView = null;
|
|
1324
1429
|
_iterator2 = _createForOfIteratorHelper(views);
|
|
1325
1430
|
_context15.prev = 5;
|
|
1326
1431
|
_loop2 = /*#__PURE__*/_regeneratorRuntime().mark(function _loop2() {
|
|
@@ -1329,7 +1434,7 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
1329
1434
|
while (1) switch (_context14.prev = _context14.next) {
|
|
1330
1435
|
case 0:
|
|
1331
1436
|
view = _step2.value;
|
|
1332
|
-
if (!
|
|
1437
|
+
if (!_this0._abortScreenshotting) {
|
|
1333
1438
|
_context14.next = 7;
|
|
1334
1439
|
break;
|
|
1335
1440
|
}
|
|
@@ -1348,35 +1453,35 @@ export var Three2D = /*#__PURE__*/function (_ThreeBase) {
|
|
|
1348
1453
|
_context14.next = 10;
|
|
1349
1454
|
return new Promise(function (resolve) {
|
|
1350
1455
|
window.setTimeout( /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee13() {
|
|
1351
|
-
var
|
|
1456
|
+
var _this0$getScreenshotS, width, height;
|
|
1352
1457
|
return _regeneratorRuntime().wrap(function _callee13$(_context13) {
|
|
1353
1458
|
while (1) switch (_context13.prev = _context13.next) {
|
|
1354
1459
|
case 0:
|
|
1355
1460
|
_context13.next = 2;
|
|
1356
|
-
return
|
|
1461
|
+
return _this0.updateView(view, {
|
|
1357
1462
|
forceUpdate: false,
|
|
1358
1463
|
updateMaterial: false
|
|
1359
1464
|
});
|
|
1360
1465
|
case 2:
|
|
1361
|
-
|
|
1466
|
+
_this0$getScreenshotS = _this0.getScreenshotSize(size), width = _this0$getScreenshotS.width, height = _this0$getScreenshotS.height;
|
|
1362
1467
|
setOrthographicCameraSize(camera, width, height);
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
camera.zoom =
|
|
1468
|
+
_this0.renderer.setSize(width, height);
|
|
1469
|
+
_this0.renderer.setPixelRatio(1);
|
|
1470
|
+
camera.zoom = _this0.getZoomForSize({
|
|
1366
1471
|
width: width,
|
|
1367
1472
|
height: height
|
|
1368
1473
|
});
|
|
1369
1474
|
camera.updateProjectionMatrix();
|
|
1370
1475
|
_context13.next = 10;
|
|
1371
|
-
return
|
|
1476
|
+
return _this0.dispatchMaterialUpdateEvent({
|
|
1372
1477
|
screenshot: true
|
|
1373
1478
|
});
|
|
1374
1479
|
case 10:
|
|
1375
1480
|
// set size and pixel ratio right before rendering, to make sure they are correct
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
data[view.identifier] =
|
|
1481
|
+
_this0.renderer.setSize(width, height);
|
|
1482
|
+
_this0.renderer.setPixelRatio(1);
|
|
1483
|
+
_this0.renderer.render(_this0.scene, camera);
|
|
1484
|
+
data[view.identifier] = _this0.renderer.domElement.toDataURL();
|
|
1380
1485
|
resolve();
|
|
1381
1486
|
case 15:
|
|
1382
1487
|
case "end":
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
|
|
2
|
+
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."); }
|
|
3
|
+
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; } }
|
|
4
|
+
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
|
|
5
|
+
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
|
|
6
|
+
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; }
|
|
7
|
+
var EPSILON = 1e-4;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Collects the vertices on the outer edge of a uv mapped geometry, in perimeter order.
|
|
11
|
+
* The design area border is stroked along the edge of the texture, so these are the vertices the
|
|
12
|
+
* border runs through. It works for any geometry whose uv attribute spans the full 0..1 range,
|
|
13
|
+
* which covers the plane, the control point grid and the open ended cylinder.
|
|
14
|
+
* @param {import("three").BufferGeometry} geometry
|
|
15
|
+
* @returns {number[]} Vertex indices, without a repeated closing vertex
|
|
16
|
+
*/
|
|
17
|
+
export var getUvBoundaryIndices = function getUvBoundaryIndices(geometry) {
|
|
18
|
+
var _geometry$attributes;
|
|
19
|
+
var uv = geometry === null || geometry === void 0 || (_geometry$attributes = geometry.attributes) === null || _geometry$attributes === void 0 ? void 0 : _geometry$attributes.uv;
|
|
20
|
+
if (!uv || !uv.count) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
var us = [];
|
|
24
|
+
var vs = [];
|
|
25
|
+
var uMin = Infinity;
|
|
26
|
+
var uMax = -Infinity;
|
|
27
|
+
var vMin = Infinity;
|
|
28
|
+
var vMax = -Infinity;
|
|
29
|
+
// a cylinder gets one segment per pixel of arc length, so this can be a few thousand vertices,
|
|
30
|
+
// which is why the extremes are collected in a loop rather than by spreading into Math.min
|
|
31
|
+
for (var i = 0; i < uv.count; i++) {
|
|
32
|
+
var u = uv.getX(i);
|
|
33
|
+
var v = uv.getY(i);
|
|
34
|
+
us.push(u);
|
|
35
|
+
vs.push(v);
|
|
36
|
+
uMin = Math.min(uMin, u);
|
|
37
|
+
uMax = Math.max(uMax, u);
|
|
38
|
+
vMin = Math.min(vMin, v);
|
|
39
|
+
vMax = Math.max(vMax, v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// a geometry without an extent in one direction has no perimeter to walk
|
|
43
|
+
if (uMax - uMin < EPSILON || vMax - vMin < EPSILON) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
var on = function on(value, target) {
|
|
47
|
+
return Math.abs(value - target) < EPSILON;
|
|
48
|
+
};
|
|
49
|
+
var collect = function collect(isOnEdge, sortBy, descending) {
|
|
50
|
+
return us.map(function (u, i) {
|
|
51
|
+
return i;
|
|
52
|
+
}).filter(function (i) {
|
|
53
|
+
return isOnEdge(us[i], vs[i]);
|
|
54
|
+
}).sort(function (a, b) {
|
|
55
|
+
return descending ? sortBy[b] - sortBy[a] : sortBy[a] - sortBy[b];
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// walk the four edges so that the result is a continuous ring
|
|
60
|
+
var ring = [].concat(_toConsumableArray(collect(function (u, v) {
|
|
61
|
+
return on(v, vMin);
|
|
62
|
+
}, us, false)), _toConsumableArray(collect(function (u) {
|
|
63
|
+
return on(u, uMax);
|
|
64
|
+
}, vs, false)), _toConsumableArray(collect(function (u, v) {
|
|
65
|
+
return on(v, vMax);
|
|
66
|
+
}, us, true)), _toConsumableArray(collect(function (u) {
|
|
67
|
+
return on(u, uMin);
|
|
68
|
+
}, vs, true)));
|
|
69
|
+
|
|
70
|
+
// the corners are part of two edges each, and the last vertex closes onto the first one
|
|
71
|
+
var deduped = ring.filter(function (index, i) {
|
|
72
|
+
return index !== ring[i - 1];
|
|
73
|
+
});
|
|
74
|
+
while (deduped.length > 1 && deduped[deduped.length - 1] === deduped[0]) {
|
|
75
|
+
deduped.pop();
|
|
76
|
+
}
|
|
77
|
+
return deduped;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Turns the boundary ring into the paths that are actually drawn on screen.
|
|
82
|
+
* Vertices facing away from the camera are culled by the renderer, so the border is not visible
|
|
83
|
+
* there and those parts must not be sampled either. The ring is treated as cyclic, so a visible
|
|
84
|
+
* run may wrap around its end.
|
|
85
|
+
* @param {number[]} indices The boundary ring, as returned by getUvBoundaryIndices
|
|
86
|
+
* @param {function(number): boolean} isVisible
|
|
87
|
+
* @param {function(number): {x: number, y: number}} project Maps a vertex index to a 2D point
|
|
88
|
+
* @returns {Array<Array<{x: number, y: number}>>} Open polylines, or one closed loop
|
|
89
|
+
*/
|
|
90
|
+
export var buildVisiblePaths = function buildVisiblePaths(indices, isVisible, project) {
|
|
91
|
+
if (indices.length < 2) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
var visible = indices.map(isVisible);
|
|
95
|
+
if (visible.every(Boolean)) {
|
|
96
|
+
// nothing is culled, so the border is a closed loop
|
|
97
|
+
var loop = indices.map(project);
|
|
98
|
+
return [[].concat(_toConsumableArray(loop), [loop[0]])];
|
|
99
|
+
}
|
|
100
|
+
if (!visible.some(Boolean)) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// start right after a culled vertex, so that no visible run is split across the array end
|
|
105
|
+
var offset = visible.indexOf(false);
|
|
106
|
+
var paths = [];
|
|
107
|
+
var current = null;
|
|
108
|
+
for (var i = 1; i <= indices.length; i++) {
|
|
109
|
+
var at = (offset + i) % indices.length;
|
|
110
|
+
if (!visible[at]) {
|
|
111
|
+
current = null;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!current) {
|
|
115
|
+
current = [];
|
|
116
|
+
paths.push(current);
|
|
117
|
+
}
|
|
118
|
+
current.push(project(indices[at]));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// a single point has no segment to sample along
|
|
122
|
+
return paths.filter(function (path) {
|
|
123
|
+
return path.length > 1;
|
|
124
|
+
});
|
|
125
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
2
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
3
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
4
|
+
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
5
|
+
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; } } }; }
|
|
6
|
+
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; } }
|
|
7
|
+
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; }
|
|
8
|
+
import { createWorkingCanvas } from "../createCanvas";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default size of the sampled band, relative to the shorter side of the sampled paths.
|
|
12
|
+
* @type {number}
|
|
13
|
+
*/
|
|
14
|
+
var RING_WIDTH_RATIO = 0.03;
|
|
15
|
+
var RING_WIDTH_MIN = 4;
|
|
16
|
+
var SAMPLE_STEP = 2;
|
|
17
|
+
/**
|
|
18
|
+
* If the sampled band is mostly transparent there is no meaningful "underlying color",
|
|
19
|
+
* so the caller should keep its static color instead.
|
|
20
|
+
* @type {number}
|
|
21
|
+
*/
|
|
22
|
+
var MIN_MEAN_ALPHA = 0.1;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Array<Array<{x: number, y: number}>>} paths
|
|
26
|
+
* @returns {{x: number, y: number, width: number, height: number}|null}
|
|
27
|
+
*/
|
|
28
|
+
var getPathsBounds = function getPathsBounds(paths) {
|
|
29
|
+
var minX = Infinity;
|
|
30
|
+
var minY = Infinity;
|
|
31
|
+
var maxX = -Infinity;
|
|
32
|
+
var maxY = -Infinity;
|
|
33
|
+
var _iterator = _createForOfIteratorHelper(paths),
|
|
34
|
+
_step;
|
|
35
|
+
try {
|
|
36
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
37
|
+
var path = _step.value;
|
|
38
|
+
var _iterator2 = _createForOfIteratorHelper(path),
|
|
39
|
+
_step2;
|
|
40
|
+
try {
|
|
41
|
+
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
42
|
+
var point = _step2.value;
|
|
43
|
+
minX = Math.min(minX, point.x);
|
|
44
|
+
minY = Math.min(minY, point.y);
|
|
45
|
+
maxX = Math.max(maxX, point.x);
|
|
46
|
+
maxY = Math.max(maxY, point.y);
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
_iterator2.e(err);
|
|
50
|
+
} finally {
|
|
51
|
+
_iterator2.f();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
_iterator.e(err);
|
|
56
|
+
} finally {
|
|
57
|
+
_iterator.f();
|
|
58
|
+
}
|
|
59
|
+
if (minX > maxX) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
x: minX,
|
|
64
|
+
y: minY,
|
|
65
|
+
width: maxX - minX,
|
|
66
|
+
height: maxY - minY
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
var getRingWidth = function getRingWidth(paths, ringWidth) {
|
|
70
|
+
if (ringWidth) {
|
|
71
|
+
return ringWidth;
|
|
72
|
+
}
|
|
73
|
+
var bounds = getPathsBounds(paths);
|
|
74
|
+
var shorterSide = bounds ? Math.min(bounds.width, bounds.height) : 0;
|
|
75
|
+
return Math.max(RING_WIDTH_MIN, RING_WIDTH_RATIO * shorterSide);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Walks every segment of the given paths and calls back with the sample points of the band that is
|
|
80
|
+
* centered on it, so the callback sees the pixels just inside and just outside the border.
|
|
81
|
+
* The band is symmetric around the segment, which is why the winding direction does not matter.
|
|
82
|
+
* @param {Array<Array<{x: number, y: number}>>} paths
|
|
83
|
+
* @param {number} ringWidth
|
|
84
|
+
* @param {number} sampleStep
|
|
85
|
+
* @param {function(number, number): void} cb Called with the x/y of every sample point
|
|
86
|
+
*/
|
|
87
|
+
var forEachRingPoint = function forEachRingPoint(paths, ringWidth, sampleStep, cb) {
|
|
88
|
+
var half = ringWidth / 2;
|
|
89
|
+
// the offsets across the band, from the inner to the outer edge
|
|
90
|
+
var offsets = [];
|
|
91
|
+
for (var o = -half; o <= half; o += sampleStep) {
|
|
92
|
+
offsets.push(o);
|
|
93
|
+
}
|
|
94
|
+
if (!offsets.length) {
|
|
95
|
+
offsets.push(0);
|
|
96
|
+
}
|
|
97
|
+
var _iterator3 = _createForOfIteratorHelper(paths),
|
|
98
|
+
_step3;
|
|
99
|
+
try {
|
|
100
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
101
|
+
var path = _step3.value;
|
|
102
|
+
for (var i = 1; i < path.length; i++) {
|
|
103
|
+
var from = path[i - 1];
|
|
104
|
+
var to = path[i];
|
|
105
|
+
var dx = to.x - from.x;
|
|
106
|
+
var dy = to.y - from.y;
|
|
107
|
+
var length = Math.sqrt(dx * dx + dy * dy);
|
|
108
|
+
if (!length) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// the unit normal of the segment, the direction the band extends into
|
|
112
|
+
var nx = -dy / length;
|
|
113
|
+
var ny = dx / length;
|
|
114
|
+
var steps = Math.max(1, Math.ceil(length / sampleStep));
|
|
115
|
+
for (var s = 0; s <= steps; s++) {
|
|
116
|
+
var t = s / steps;
|
|
117
|
+
var x = from.x + dx * t;
|
|
118
|
+
var y = from.y + dy * t;
|
|
119
|
+
var _iterator4 = _createForOfIteratorHelper(offsets),
|
|
120
|
+
_step4;
|
|
121
|
+
try {
|
|
122
|
+
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
|
|
123
|
+
var _o = _step4.value;
|
|
124
|
+
cb(x + nx * _o, y + ny * _o);
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
_iterator4.e(err);
|
|
128
|
+
} finally {
|
|
129
|
+
_iterator4.f();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
_iterator3.e(err);
|
|
136
|
+
} finally {
|
|
137
|
+
_iterator3.f();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Averages the pixels of the border band over the given image data.
|
|
143
|
+
* The channels are weighted by alpha, so semi transparent pixels contribute proportionally.
|
|
144
|
+
* This is kept free of any canvas access so it can be tested without a DOM canvas implementation.
|
|
145
|
+
* @param {{width: number, height: number, data: Uint8ClampedArray|number[]}} imageData
|
|
146
|
+
* @param {{x: number, y: number}} origin Position of the image data within the source image
|
|
147
|
+
* @param {Array<Array<{x: number, y: number}>>} paths The border, in source image pixels
|
|
148
|
+
* @param {{[ringWidth]: number, [sampleStep]: number, [minAlpha]: number}} [options]
|
|
149
|
+
* @returns {{r: number, g: number, b: number}|null} null if there is nothing meaningful to sample
|
|
150
|
+
*/
|
|
151
|
+
export var averageRingColorFromImageData = function averageRingColorFromImageData(imageData, origin, paths) {
|
|
152
|
+
var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
153
|
+
var _options$sampleStep = options.sampleStep,
|
|
154
|
+
sampleStep = _options$sampleStep === void 0 ? SAMPLE_STEP : _options$sampleStep,
|
|
155
|
+
_options$minAlpha = options.minAlpha,
|
|
156
|
+
minAlpha = _options$minAlpha === void 0 ? MIN_MEAN_ALPHA : _options$minAlpha;
|
|
157
|
+
var ringWidth = getRingWidth(paths, options.ringWidth);
|
|
158
|
+
var width = imageData.width,
|
|
159
|
+
height = imageData.height,
|
|
160
|
+
data = imageData.data;
|
|
161
|
+
var wr = 0;
|
|
162
|
+
var wg = 0;
|
|
163
|
+
var wb = 0;
|
|
164
|
+
var wa = 0;
|
|
165
|
+
var count = 0;
|
|
166
|
+
forEachRingPoint(paths, ringWidth, sampleStep, function (px, py) {
|
|
167
|
+
var x = Math.round(px - origin.x);
|
|
168
|
+
var y = Math.round(py - origin.y);
|
|
169
|
+
if (x < 0 || y < 0 || x >= width || y >= height) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
var i = (y * width + x) * 4;
|
|
173
|
+
var a = data[i + 3];
|
|
174
|
+
wr += data[i] * a;
|
|
175
|
+
wg += data[i + 1] * a;
|
|
176
|
+
wb += data[i + 2] * a;
|
|
177
|
+
wa += a;
|
|
178
|
+
count++;
|
|
179
|
+
});
|
|
180
|
+
if (!count || wa / (count * 255) < minAlpha) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
r: Math.round(wr / wa),
|
|
185
|
+
g: Math.round(wg / wa),
|
|
186
|
+
b: Math.round(wb / wa)
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Bounding box of the given paths, grown by half the band width and clamped to the source.
|
|
192
|
+
* @param {{width: number, height: number}} source
|
|
193
|
+
* @param {Array<Array<{x: number, y: number}>>} paths
|
|
194
|
+
* @param {number} ringWidth
|
|
195
|
+
* @returns {{x: number, y: number, width: number, height: number}}
|
|
196
|
+
*/
|
|
197
|
+
export var getSampleBounds = function getSampleBounds(source, paths, ringWidth) {
|
|
198
|
+
var bounds = getPathsBounds(paths);
|
|
199
|
+
if (!bounds) {
|
|
200
|
+
return {
|
|
201
|
+
x: 0,
|
|
202
|
+
y: 0,
|
|
203
|
+
width: 0,
|
|
204
|
+
height: 0
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
var half = ringWidth / 2;
|
|
208
|
+
var x = Math.max(0, Math.floor(bounds.x - half));
|
|
209
|
+
var y = Math.max(0, Math.floor(bounds.y - half));
|
|
210
|
+
var right = Math.min(source.width, Math.ceil(bounds.x + bounds.width + half));
|
|
211
|
+
var bottom = Math.min(source.height, Math.ceil(bounds.y + bounds.height + half));
|
|
212
|
+
return {
|
|
213
|
+
x: x,
|
|
214
|
+
y: y,
|
|
215
|
+
width: right - x,
|
|
216
|
+
height: bottom - y
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Reads the average color of the band that the design area border is drawn on.
|
|
222
|
+
* @param {HTMLImageElement|HTMLCanvasElement} source The product image, in its own pixel space
|
|
223
|
+
* @param {Array<Array<{x: number, y: number}>>} paths The border, in the pixel space of the source.
|
|
224
|
+
* Every path is a polyline, a closed one repeats its first point at the end.
|
|
225
|
+
* @param {{[ringWidth]: number, [sampleStep]: number, [minAlpha]: number}} [options]
|
|
226
|
+
* @returns {{r: number, g: number, b: number}|null} null if the color could not be determined
|
|
227
|
+
*/
|
|
228
|
+
export var sampleRingAverage = function sampleRingAverage(source, paths) {
|
|
229
|
+
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
230
|
+
if (!source || !source.width || !source.height || !paths || !paths.length) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
var bounds = getSampleBounds(source, paths, getRingWidth(paths, options.ringWidth));
|
|
234
|
+
if (bounds.width <= 0 || bounds.height <= 0) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
var _createWorkingCanvas = createWorkingCanvas(source, true),
|
|
238
|
+
_createWorkingCanvas2 = _slicedToArray(_createWorkingCanvas, 2),
|
|
239
|
+
ctx = _createWorkingCanvas2[1];
|
|
240
|
+
var imageData;
|
|
241
|
+
try {
|
|
242
|
+
imageData = ctx.getImageData(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
// a cross origin image without the proper headers taints the canvas, in that case the
|
|
245
|
+
// caller has to fall back to its static color
|
|
246
|
+
// eslint-disable-next-line no-console
|
|
247
|
+
console.warn("Could not read the product image pixels to derive the border color.", e);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return averageRingColorFromImageData(imageData, bounds, paths, options);
|
|
251
|
+
};
|