diff --git a/package.json b/package.json index 3bd7132..174059a 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,15 @@ "react": "^18.0.0" }, "dependencies": { + "@types/pako": "^2.0.4", + "@types/three": "^0.184.0", + "axios": "^1.15.2", "javascript-obfuscator": "^5.4.1", "lit": "^3.2.0", - "rollup-plugin-obfuscator": "^1.1.0" + "pako": "^2.1.0", + "pure-rand": "^8.4.0", + "rollup-plugin-obfuscator": "^1.1.0", + "three": "^0.184.0" }, "devDependencies": { "@11ty/eleventy": "^1.0.1", diff --git a/src/Components/RoomView/helpers/PhotographicView.ts b/src/Components/RoomView/helpers/PhotographicView.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/RoomView/helpers/RoomViewHelper.ts b/src/Components/RoomView/helpers/RoomViewHelper.ts new file mode 100644 index 0000000..868fd2a --- /dev/null +++ b/src/Components/RoomView/helpers/RoomViewHelper.ts @@ -0,0 +1,1780 @@ +import * as THREE from 'three'; +import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js'; +import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader.js'; +import { + canvasToBlobPromise, + clearCanvas, + createCanvas, +} from '../../../Utils/canvasutils'; +import { + CarpetRatio, + MyRoomPointsOBJ, + PointsHistory, + ScaleFactor, + Vector2Pool, + Vector3Pool, +} from '../types'; + +declare const convertUnit: ( + fromUnit: string, + toUnit: string, + value: number, + decimals?: number +) => number; +declare const convertArrIntoRad: (arr: number[]) => [number, number, number]; +declare const areaOfellipse: (a: number, b: number) => number; +declare const createVector: ( + vertex: THREE.Vector3, + camera: THREE.Camera, + width: number, + height: number +) => {x: number; y: number}; +declare const resizeKeepingAspect: ( + image: HTMLImageElement, + windowSize: {width: number; height: number} +) => {width: number; height: number}; +declare const readImage: (url: string) => Promise; + +declare const AppProvider: { + uploadMyRoom: (blob: Blob) => Promise; + uploadMyRoomMask: (params: {maskUrl: Blob; roomId: string}) => Promise; + saveAsRoom: (params: { + mode: string; + roomId: string; + file: unknown; + props: unknown; + floorpoints: {x: number; y: number}[]; + notfloorpoints: {x: number; y: number}[]; + carpetpoints: {x: number; y: number}[]; + }) => Promise; +}; + +const fbxLoader = new FBXLoader(); + +interface DesignData { + Unit: string; + PhysicalWidth: number; + PhysicalHeight: number; + IsIrregular?: boolean; +} + +interface WindowExtended extends Window { + InterfaceElements: {IsWallToWall: boolean}; + flags: { + visualizations?: { + wallToWallCenterRepeat?: {x?: boolean; y?: boolean}; + }; + inhouseChanges?: { + saveFunctionForFlutter?: boolean; + }; + }; + constraints?: {audio: boolean; video: boolean}; + initialData?: {customDesignUrl?: string}; + getByteData?: string; +} + +interface FloorCenter { + x: string; + y: string; + area?: string; + no_floor?: boolean; +} + +interface Orientation { + hor: string; + vert: string; + exception?: boolean; + flag?: boolean; +} + +export interface AnalysisData { + floor_center: FloorCenter; + orientation: Orientation; + fov: string; +} + +interface FloorBounds { + minX: number; + maxX: number; + minZ: number; + maxZ: number; +} + +interface CarpetDimensions { + width: number; + depth: number; +} + +export class RoomViewHelper { + scene: THREE.Scene; + textureLoader: THREE.TextureLoader; + offset: THREE.Vector3; + fbxLoaded: boolean; + surfaceName: string; + + zoomCarpetStep: number; + maxZoom: number; + minZoom: number; + carpetRatio: CarpetRatio; + origCarpetDims: {w: number; h: number}; + scaleFactor: ScaleFactor; + pileHeight: number; + + myRoomPointsOBJ: MyRoomPointsOBJ; + pointsHistory: PointsHistory; + pointsHistoryArray: string[]; + + constraints: {audio: boolean; video: boolean}; + + initCarpetPos: number[] | null; + initCarpetRot: number[] | null; + initCarpetScale: number[] | null; + designDetails: DesignData | null; + designPath: string | null; + canvasInitiated: boolean; + bgCanvas!: HTMLCanvasElement; + rendererCanvas!: HTMLCanvasElement; + maskCanvas!: HTMLCanvasElement; + gizmoCanvas!: HTMLCanvasElement; + inputCanvas!: HTMLCanvasElement; + container!: HTMLElement; + bgImage!: HTMLImageElement; + origWid!: number; + origHgt!: number; + roomAnalysisData: AnalysisData | null; + ratioWid!: number; + ratioHgt!: number; + myroomInputSelected: string = ''; + roomdataurl: string = ''; + origBG: string = ''; + + intersectsGizmo: boolean = false; + prev: {x: number; y: number} = {x: 0, y: 0}; + + _vector3Pool: Vector3Pool; + _vector2Pool: Vector2Pool; + _raycaster: THREE.Raycaster; + + _cachedFloorBounds: FloorBounds | null; + _cachedCarpetDims: CarpetDimensions | null; + _cacheInvalidated: boolean; + + _tempPositionMesh?: THREE.Mesh; + + carpetMesh?: THREE.Mesh; + bounds?: THREE.Box3; + material?: THREE.MeshBasicMaterial; + carpetRatioNew?: CarpetRatio; + + dims!: {width: number; height: number}; + fov: number = 45; + renderer!: THREE.WebGLRenderer; + camera!: THREE.PerspectiveCamera; + orbit!: OrbitControls; + + constructor() { + this.scene = new THREE.Scene(); + this.textureLoader = new THREE.TextureLoader(); + this.offset = new THREE.Vector3(); + this.fbxLoaded = false; + this.surfaceName = 'Box002'; + + this.zoomCarpetStep = 0.05; + this.maxZoom = 2; + this.minZoom = 0.5; + this.carpetRatio = {w: 1, h: 1}; + this.origCarpetDims = {w: 10, h: 10}; + this.scaleFactor = {x: 0, y: 0}; + this.pileHeight = 0.25; + + this.myRoomPointsOBJ = { + floorpoints: [], + notfloorpoints: [], + carpetpoints: [], + }; + + this.pointsHistory = { + floorpoints: [], + notfloorpoints: [], + inputSelected: '', + }; + this.pointsHistoryArray = []; + + this.constraints = (window as unknown as WindowExtended).constraints = { + audio: false, + video: true, + }; + + this.initCarpetPos = null; + this.initCarpetRot = null; + this.initCarpetScale = null; + this.designDetails = null; + this.designPath = null; + this.canvasInitiated = false; + this.roomAnalysisData = null; + + this._vector3Pool = { + v1: new THREE.Vector3(), + v2: new THREE.Vector3(), + v3: new THREE.Vector3(), + v4: new THREE.Vector3(), + }; + this._vector2Pool = { + v1: new THREE.Vector2(), + v2: new THREE.Vector2(), + }; + this._raycaster = new THREE.Raycaster(); + + this._cachedFloorBounds = null; + this._cachedCarpetDims = null; + this._cacheInvalidated = true; + } + + initCanvas({ + bgCanvas, + rendererCanvas, + maskCanvas, + gizmoCanvas, + inputCanvas, + container, + }: { + bgCanvas: HTMLCanvasElement; + rendererCanvas: HTMLCanvasElement; + maskCanvas: HTMLCanvasElement; + gizmoCanvas: HTMLCanvasElement; + inputCanvas: HTMLCanvasElement; + container: HTMLElement; + }) { + this.canvasInitiated = true; + this.designPath = null; + this.bgCanvas = bgCanvas; + this.rendererCanvas = rendererCanvas; + this.maskCanvas = maskCanvas; + this.gizmoCanvas = gizmoCanvas; + this.inputCanvas = inputCanvas; + this.container = container; + } + + updateBackground({ + bgImage, + width, + height, + }: { + bgImage: HTMLImageElement; + width: number; + height: number; + }) { + this.bgImage = bgImage; + const bgCtx = this.bgCanvas.getContext('2d'); + this.bgCanvas.width = width; + this.bgCanvas.height = height; + this.origWid = width; + this.origHgt = height; + this.scaleFactor = {x: 0, y: 0}; + clearCanvas(this.bgCanvas, width, height); + if (bgCtx) bgCtx.drawImage(bgImage, 0, 0, width, height); + } + + initScene({ + dims, + }: { + dims: {width: number; height: number}; + sceneConfig?: Record; + }) { + this.dims = dims; + const {width, height} = this.dims; + this.renderer = new THREE.WebGLRenderer({ + canvas: this.rendererCanvas, + preserveDrawingBuffer: true, + alpha: true, + antialias: false, + }); + this.renderer.setPixelRatio(devicePixelRatio); + this.renderer.setSize(width, height); + this.camera = new THREE.PerspectiveCamera( + this.fov, + width / height, + 0.1, + 10000 + ); + this.camera.position.set(0, 160, 386); + + this.orbit = new OrbitControls(this.camera, this.renderer.domElement); + this.orbit.screenSpacePanning = true; + this.orbit.enabled = true; + this.orbit.minPolarAngle = -0.3490658503988659; + this.orbit.maxPolarAngle = 1.7453292519943295; + this.orbit.target = new THREE.Vector3(0, 0, 0); + this.orbit.addEventListener('change', () => { + this.renderer.render(this.scene, this.camera); + }); + return Promise.resolve(); + } + + setRoomAnalysis(analysisData: AnalysisData) { + this.roomAnalysisData = analysisData; + this._invalidateCache(); + } + + _invalidateCache() { + this._cachedFloorBounds = null; + this._cachedCarpetDims = null; + this._cacheInvalidated = true; + } + + render() { + this.renderer.render(this.scene, this.camera); + } + + setupCarpet(_fbxUrl: string, _shouldSetPosition: boolean): void {} + + setCarpetVisibility(visible: boolean) { + if (this.carpetMesh) this.carpetMesh.visible = visible; + this.render(); + } + + loadDesignCanvas({ + designCanvas, + fbxUrl, + designDetails, + designPath, + customWid, + customHgt, + unit, + }: { + designCanvas: HTMLCanvasElement; + fbxUrl: string; + designDetails: DesignData; + designPath: string; + customWid?: number; + customHgt?: number; + unit: string; + }) { + const shouldResetPosRot = this.designDetails === null; + const shouldResetScale = + this.designPath !== designPath || (customWid && customHgt); + + if ( + this.designDetails !== designDetails || + this.designPath !== designPath + ) { + this._invalidateCache(); + } + + this.designDetails = designDetails; + this.designPath = designPath; + + return new Promise((resolve, reject) => { + const designTexture = new THREE.CanvasTexture(designCanvas); + designTexture.anisotropy = this.renderer.capabilities.getMaxAnisotropy(); + designTexture.colorSpace = THREE.SRGBColorSpace; + + let position = [0, 0, 0]; + let rotation = [-90, 0, 90]; + + if (this.roomAnalysisData && this.roomAnalysisData.floor_center) { + position = this.calculateCarpetPosition(this.roomAnalysisData); + rotation = this.calculateCarpetRotation(this.roomAnalysisData); + } + + let repeat = [1, 1]; + + const floorBounds = this._getFloorBoundsCached(); + if (floorBounds) { + this.bounds = new THREE.Box3( + new THREE.Vector3( + floorBounds.minX, + position[1] - 30, + floorBounds.minZ + ), + new THREE.Vector3( + floorBounds.maxX, + position[1] + 30, + floorBounds.maxZ + ) + ); + } + + const win = window as unknown as WindowExtended; + + if (win.InterfaceElements.IsWallToWall) { + const walltowalldims = [100, 100]; + if (designDetails.Unit !== 'ft') { + const convertedUnitWidth = convertUnit( + designDetails.Unit, + 'ft', + designDetails.PhysicalWidth, + 2 + ); + const convertedUnitHeight = convertUnit( + designDetails.Unit, + 'ft', + designDetails.PhysicalHeight, + 2 + ); + repeat = [ + walltowalldims[0] / convertedUnitWidth, + walltowalldims[1] / convertedUnitHeight, + ]; + } else { + repeat = [ + walltowalldims[0] / designDetails.PhysicalWidth, + walltowalldims[1] / designDetails.PhysicalHeight, + ]; + } + + let offsetX = 0; + let offsetY = 0; + if (win.flags.visualizations?.wallToWallCenterRepeat?.x) { + const halfRepeatX = repeat[0] / 2; + offsetX = 0.5 - (halfRepeatX - Math.floor(halfRepeatX)); + } + if (win.flags.visualizations?.wallToWallCenterRepeat?.y) { + const halfRepeatY = repeat[1] / 2; + offsetY = 0.5 - (halfRepeatY - Math.floor(halfRepeatY)); + } + designTexture.offset.fromArray([offsetX, offsetY]); + designTexture.wrapS = designTexture.wrapT = THREE.RepeatWrapping; + designTexture.repeat.fromArray([repeat[0], repeat[1]]); + designTexture.colorSpace = THREE.SRGBColorSpace; + + const z = + (1.25 * + 107.6 * + 10 * + convertUnit( + designDetails.Unit, + 'ft', + designDetails.PhysicalHeight + ) * + (1 + this.scaleFactor.y)) / + walltowalldims[1]; + const x = + (1.25 * + 107.6 * + 10 * + convertUnit(designDetails.Unit, 'ft', designDetails.PhysicalWidth) * + (1 + this.scaleFactor.x)) / + walltowalldims[0]; + + this.bounds = new THREE.Box3( + new THREE.Vector3(position[0] - x, position[1] - 30, position[2] - z), + new THREE.Vector3(position[0] + x, position[1] + 30, position[2] + z) + ); + } + + this.material = new THREE.MeshBasicMaterial({ + map: designTexture, + transparent: true, + }); + this.material.needsUpdate = true; + + let IsIrregular: boolean | undefined; + const PhysicalWidth = + convertUnit(designDetails.Unit, 'ft', designDetails.PhysicalWidth) * + repeat[0]; + const PhysicalHeight = + convertUnit(designDetails.Unit, 'ft', designDetails.PhysicalHeight) * + repeat[1]; + IsIrregular = designDetails.IsIrregular; + + if ( + this.fbxLoaded && + (this.origCarpetDims.w !== PhysicalWidth || + this.origCarpetDims.h !== PhysicalHeight) + ) { + this.carpetRatioNew = { + w: PhysicalWidth / this.origCarpetDims.w, + h: PhysicalHeight / this.origCarpetDims.h, + }; + if (!shouldResetScale && this.carpetMesh && this.carpetRatioNew) { + this.initCarpetScale = [ + this.carpetRatioNew.w + this.scaleFactor.x * this.carpetRatioNew.w, + this.carpetRatioNew.h + this.scaleFactor.y * this.carpetRatioNew.h, + IsIrregular ? 0.1 : this.pileHeight, + ]; + this.carpetMesh.scale.set( + ...(this.initCarpetScale as [number, number, number]) + ); + } + } + + this.carpetRatio = { + w: PhysicalWidth / this.origCarpetDims.w, + h: PhysicalHeight / this.origCarpetDims.h, + }; + + const setup = () => { + if (!this.carpetMesh) return; + + if (shouldResetPosRot) { + this.initCarpetPos = position; + this.initCarpetRot = rotation; + this.carpetMesh.position.fromArray(position); + this.carpetMesh.rotation.fromArray(convertArrIntoRad(rotation)); + } + if (shouldResetScale) { + this.initCarpetScale = [ + this.carpetRatio.w + this.scaleFactor.x * this.carpetRatio.w, + this.carpetRatio.h + this.scaleFactor.y * this.carpetRatio.h, + IsIrregular ? 0.1 : this.pileHeight, + ]; + + if (customWid && customHgt) { + const localWid = convertUnit(unit, 'ft', customWid); + const localHgt = convertUnit(unit, 'ft', customHgt); + this.carpetMesh.scale.set( + localWid / 10, + localHgt / 10, + IsIrregular ? 0.1 : this.pileHeight + ); + } else { + this.carpetMesh.scale.set( + ...(this.initCarpetScale as [number, number, number]) + ); + } + } + + if (this.material) { + this.carpetMesh.material = this.material; + this.carpetMesh.material.needsUpdate = true; + } + this.fbxLoaded = true; + this.render(); + resolve(); + }; + + if (!this.fbxLoaded) { + fbxLoader.load( + fbxUrl, + (obj) => { + this.carpetMesh = obj.getObjectByName( + this.surfaceName + ) as THREE.Mesh; + this.scene.add(this.carpetMesh); + this.setCarpetVisibility(false); + setup(); + }, + undefined, + (err) => reject(err) + ); + } else { + setup(); + } + }); + } + + updateMap() { + if (!this.carpetMesh) return; + const mat = this.carpetMesh.material as THREE.MeshBasicMaterial; + if (mat.map) mat.map.needsUpdate = true; + this.render(); + } + + calculateCarpetPosition(analysisData: AnalysisData): number[] { + if (!analysisData?.floor_center) return [0, 0, 0]; + + const {floor_center} = analysisData; + if (floor_center.no_floor) return [0, 0, 0]; + if (floor_center.area && parseInt(floor_center.area) < 10000) + return [0, 0, 0]; + if (!this.camera || !this.renderer || !this.origWid || !this.origHgt) + return [0, 0, 0]; + + try { + const normalizedX = (parseFloat(floor_center.x) / this.origWid) * 2 - 1; + const normalizedY = -(parseFloat(floor_center.y) / this.origHgt) * 2 + 1; + + const normVec = this._vector2Pool.v1.set(normalizedX, normalizedY); + this._raycaster.setFromCamera(normVec, this.camera); + + const floorPlane = new THREE.Plane(this._vector3Pool.v1.set(0, 1, 0), 0); + const intersectionPoint = this._vector3Pool.v2; + const hasIntersection = this._raycaster.ray.intersectPlane( + floorPlane, + intersectionPoint + ); + + if (hasIntersection) { + const floorBounds = this._getFloorBoundsCached(); + const carpetDims = this._getCarpetDimensionsCached(); + const finalPosition = this._vector3Pool.v3.copy(intersectionPoint); + + if (floorBounds && carpetDims) { + const safetyMargin = 1.05; + const halfWidth = (carpetDims.width / 2) * safetyMargin; + const halfDepth = (carpetDims.depth / 2) * safetyMargin; + finalPosition.x = Math.max( + floorBounds.minX + halfWidth, + Math.min(floorBounds.maxX - halfWidth, finalPosition.x) + ); + finalPosition.z = Math.max( + floorBounds.minZ + halfDepth, + Math.min(floorBounds.maxZ - halfDepth, finalPosition.z) + ); + } else { + const centerPoint = this._vector3Pool.v4; + this._raycaster.setFromCamera( + this._vector2Pool.v2.set(0, 0), + this.camera + ); + const centerIntersection = this._raycaster.ray.intersectPlane( + new THREE.Plane(this._vector3Pool.v1.set(0, 1, 0), 0), + centerPoint + ); + if (centerIntersection) finalPosition.copy(centerPoint); + } + + const distanceToCamera = Math.sqrt( + Math.pow(finalPosition.x - this.camera.position.x, 2) + + Math.pow(finalPosition.z - this.camera.position.z, 2) + ); + + if (distanceToCamera > 500) { + const centerPoint = this._vector3Pool.v4; + this._raycaster.setFromCamera( + this._vector2Pool.v2.set(0, 0), + this.camera + ); + if ( + this._raycaster.ray.intersectPlane( + new THREE.Plane(this._vector3Pool.v1.set(0, 1, 0), 0), + centerPoint + ) + ) { + return [centerPoint.x, 0, centerPoint.z]; + } + } + + return [finalPosition.x, 0, finalPosition.z]; + } + + const centerPoint = this._vector3Pool.v2; + this._raycaster.setFromCamera( + this._vector2Pool.v1.set(0, 0), + this.camera + ); + if ( + this._raycaster.ray.intersectPlane( + new THREE.Plane(this._vector3Pool.v3.set(0, 1, 0), 0), + centerPoint + ) + ) { + return [centerPoint.x, 0, centerPoint.z]; + } + return [0, 0, 0]; + } catch (error) { + console.error('Error in calculateCarpetPosition:', error); + return [0, 0, 0]; + } + } + + _getFloorBoundsCached(): FloorBounds | null { + if (this._cachedFloorBounds && !this._cacheInvalidated) + return this._cachedFloorBounds; + this._cachedFloorBounds = this._estimateFloorBounds(); + return this._cachedFloorBounds; + } + + _getCarpetDimensionsCached(): CarpetDimensions | null { + if (this._cachedCarpetDims && !this._cacheInvalidated) + return this._cachedCarpetDims; + this._cachedCarpetDims = this._estimateCarpetDimensions(); + this._cacheInvalidated = false; + return this._cachedCarpetDims; + } + + _estimateFloorBounds(): FloorBounds | null { + if (!this.camera || !this.renderer) return null; + + try { + const floorPlane = new THREE.Plane(this._vector3Pool.v1.set(0, 1, 0), 0); + const testPointsData = [ + -0.9, -0.9, 0.9, -0.9, -0.9, 0.9, 0.9, 0.9, 0, -0.9, -0.9, 0, 0.9, 0, 0, + 0, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, + ]; + + let minX = Infinity, + maxX = -Infinity; + let minZ = Infinity, + maxZ = -Infinity; + let validIntersections = 0; + + const testPoint = this._vector2Pool.v2; + const intersection = this._vector3Pool.v2; + + for (let i = 0; i < testPointsData.length; i += 2) { + testPoint.set(testPointsData[i], testPointsData[i + 1]); + this._raycaster.setFromCamera(testPoint, this.camera); + if (this._raycaster.ray.intersectPlane(floorPlane, intersection)) { + minX = Math.min(minX, intersection.x); + maxX = Math.max(maxX, intersection.x); + minZ = Math.min(minZ, intersection.z); + maxZ = Math.max(maxZ, intersection.z); + validIntersections++; + } + } + + if (validIntersections < 1) return null; + + const paddingX = (maxX - minX) * 0.03; + const paddingZ = (maxZ - minZ) * 0.03; + return { + minX: minX + paddingX, + maxX: maxX - paddingX, + minZ: minZ + paddingZ, + maxZ: maxZ - paddingZ, + }; + } catch (error) { + console.warn('Failed to estimate floor bounds:', error); + return null; + } + } + + _estimateCarpetDimensions(): CarpetDimensions | null { + if (!this.designDetails) return null; + try { + const PhysicalWidth = convertUnit( + this.designDetails.Unit, + 'ft', + this.designDetails.PhysicalWidth + ); + const PhysicalHeight = convertUnit( + this.designDetails.Unit, + 'ft', + this.designDetails.PhysicalHeight + ); + return {width: PhysicalWidth * 10, depth: PhysicalHeight * 10}; + } catch (error) { + console.error('Error in _estimateCarpetDimensions:', error); + return null; + } + } + + _isRoomLandscape(): boolean { + const floorBounds = this._getFloorBoundsCached(); + if (floorBounds) { + return ( + floorBounds.maxX - floorBounds.minX > + floorBounds.maxZ - floorBounds.minZ + ); + } + if (this.origWid && this.origHgt) return this.origWid > this.origHgt; + return true; + } + + _isCarpetLandscape(): boolean { + if (!this.designDetails) return true; + try { + const PhysicalWidth = convertUnit( + this.designDetails.Unit, + 'ft', + this.designDetails.PhysicalWidth + ); + const PhysicalHeight = convertUnit( + this.designDetails.Unit, + 'ft', + this.designDetails.PhysicalHeight + ); + return PhysicalWidth > PhysicalHeight; + } catch (error) { + console.error('Error in _isCarpetLandscape:', error); + return true; + } + } + + calculateCarpetRotation(analysisData: AnalysisData): number[] { + const defaultRotation = [-90, 0, 90]; + + if (!analysisData?.orientation) { + console.warn('Room orientation data incomplete, using default rotation'); + return defaultRotation; + } + + const {orientation} = analysisData; + if (orientation.exception) { + console.warn('Orientation detection failed, using default rotation'); + return defaultRotation; + } + + try { + const rotX = -90; + const rotY = 0; + + let horAngle = parseFloat(orientation.hor); + const horAngleSnapTolerance = 10; + if (Math.abs(horAngle) < horAngleSnapTolerance) horAngle = 0; + if (Math.abs(horAngle - 90) < horAngleSnapTolerance) horAngle = 90; + if (Math.abs(horAngle + 90) < horAngleSnapTolerance) horAngle = -90; + + let rotZ = 90 - horAngle; + if (!orientation.flag) rotZ += 90; + + if (this._isRoomLandscape() !== this._isCarpetLandscape()) rotZ += 90; + + while (rotZ > 180) rotZ -= 360; + while (rotZ < -180) rotZ += 360; + + return [rotX, rotY, rotZ]; + } catch (error) { + console.error('Error calculating carpet rotation:', error); + return defaultRotation; + } + } + + uploadRoom({bgCanvas}: {bgCanvas: HTMLCanvasElement}) { + return canvasToBlobPromise(bgCanvas).then((bgBlob) => { + return AppProvider.uploadMyRoom(bgBlob).then((roomId) => { + if (roomId === '' || roomId === 'maxsize') return roomId; + return {roomId}; + }); + }); + } + + updateMask = async ({ + maskUrl, + maskImage, + maskCanvas, + bgCanvas, + width, + height, + }: { + maskUrl?: string; + maskImage?: HTMLImageElement; + maskCanvas: HTMLCanvasElement; + bgCanvas: HTMLCanvasElement; + width?: number; + height?: number; + }) => { + const maskRoomImage = maskUrl ? await readImage(maskUrl) : maskImage!; + width = width || maskCanvas.width; + height = height || maskCanvas.height; + maskCanvas.width = width; + maskCanvas.height = height; + clearCanvas(maskCanvas, width, height); + const tmpCanvas = createCanvas(width, height); + tmpCanvas.getContext('2d')!.drawImage(bgCanvas, 0, 0, width, height); + this.makeMask(tmpCanvas, width, height, maskRoomImage); + maskCanvas.getContext('2d')!.drawImage(tmpCanvas, 0, 0, width, height); + }; + + makeMask( + canvas: HTMLCanvasElement, + w: number, + h: number, + maskImg: HTMLImageElement, + flag = false + ) { + const tCanvas = createCanvas(w, h); + const shCtx = canvas.getContext('2d')!; + const tmpCtx = tCanvas.getContext('2d')!; + tmpCtx.drawImage(maskImg, 0, 0, w, h); + const imgData = shCtx.getImageData(0, 0, w, h); + const maskData = tmpCtx.getImageData(0, 0, w, h); + for (let i = 0; i < maskData.data.length; i += 4) { + imgData.data[i + 3] = flag ? maskData.data[i] : 255 - maskData.data[i]; + } + shCtx.putImageData(imgData, 0, 0); + this.roomdataurl = canvas.toDataURL('image/png'); + return maskData; + } + + async uploadMask({maskUrl, roomId}: {maskUrl: string; roomId: string}) { + if (!maskUrl || !roomId) return; + return new Promise((resolve) => { + readImage(maskUrl).then((maskImage512) => { + const tmpCanvas = createCanvas(maskImage512.width, maskImage512.height); + tmpCanvas.getContext('2d')!.drawImage(maskImage512, 0, 0); + canvasToBlobPromise(tmpCanvas).then((mask512Blob) => { + AppProvider.uploadMyRoomMask({maskUrl: mask512Blob, roomId}).then( + () => resolve() + ); + }); + }); + }); + } + + resetCarpetTransform() { + if ( + !this.carpetMesh || + !this.initCarpetPos || + !this.initCarpetRot || + !this.initCarpetScale + ) + return; + this.carpetMesh.position.fromArray(this.initCarpetPos); + this.carpetMesh.rotation.fromArray(convertArrIntoRad(this.initCarpetRot)); + this.carpetMesh.scale.set( + ...(this.initCarpetScale as [number, number, number]) + ); + this.render(); + } + + rotateCarpet(angleinDeg: number) { + if (!this.carpetMesh) return; + this.carpetMesh.rotation.z += (angleinDeg * Math.PI) / 180; + this.render(); + } + + scaleUpCarpet() { + this.scaleCarpet(this.zoomCarpetStep); + } + + scaleDownCarpet() { + this.scaleCarpet(-this.zoomCarpetStep); + } + + scaleCarpet(step: number) { + if (!this.carpetMesh) return; + const newValue = + (this.carpetMesh.scale.x + step * this.carpetRatio.w) / + this.carpetRatio.w; + if (this.maxZoom >= newValue && newValue >= this.minZoom) { + this.carpetMesh.scale.x += step * this.carpetRatio.w; + this.carpetMesh.scale.y += step * this.carpetRatio.h; + this.scaleFactor.x += step; + this.scaleFactor.y += step; + this.render(); + } + } + + adjustPlaneHeight(increaseBool: boolean, step = 5) { + const deltaY = !increaseBool ? +step : -step; + (this.orbit as any).pan(0, deltaY); + this.orbit.update(); + } + + adjustCameraAngle(increaseBool: boolean, step = 2) { + const delta = ((increaseBool ? +step : -step) * Math.PI) / 180; + this.orbit.rotateUp(delta); + this.orbit.update(); + } + + mouseDownTouchStart(e: {x: number; y: number}) { + if (!this.carpetMesh) return; + this.intersectsGizmo = this.findGizmoIntersection(e); + this.prev = {...e}; + if (!this.intersectsGizmo) { + const intersect = this.raycastMouseOnObject(e); + if (!intersect) return; + const objPos = this.carpetMesh.position.clone(); + this.offset.copy(intersect.point).sub(objPos); + this.render(); + } + } + + mouseTouchMove(e: {x: number; y: number}, translateCarpetMode: boolean) { + if (!this.carpetMesh) return; + if (translateCarpetMode) { + if (!this.intersectsGizmo) { + const intersect = this.raycastMouseOnObject(e); + if (!intersect) return; + const objPos = this.carpetMesh.position.clone(); + const sub = intersect.point.sub(this.offset); + sub.y = objPos.y; + this.carpetMesh.position.copy(sub); + this.render(); + this.updateGizmo(); + } else { + const difference = this.prev.x - e.x; + this.carpetMesh.rotation.z -= (difference * Math.PI) / 180; + this.prev = {...e}; + this.render(); + } + } else { + const difference = this.prev.y - e.y; + this.adjustCameraAngle(false, difference); + this.prev = {...e}; + } + } + + getRendererOffset() { + return { + offsetX: this.renderer.domElement.offsetLeft, + offsetY: this.renderer.domElement.offsetTop, + }; + } + + raycastMouseOnObject(e: {x: number; y: number}) { + if (!this.carpetMesh) return undefined; + const mouse = this.convMouseCord(e); + const raycaster = new THREE.Raycaster(); + raycaster.setFromCamera(mouse, this.camera); + return raycaster.intersectObject(this.carpetMesh)[0]; + } + + convMouseCord(e: {x: number; y: number}) { + const vec = new THREE.Vector2(); + this.renderer.getSize(vec); + return new THREE.Vector2((e.x / vec.x) * 2 - 1, -(e.y / vec.y) * 2 + 1); + } + + getCarpetPositions() { + if (!this.carpetMesh) return; + this.carpetMesh.geometry.computeBoundingBox(); + const box = this.carpetMesh.geometry.boundingBox!; + + const vertex1 = this._vector3Pool.v1.copy(box.min); + const vertex2 = this._vector3Pool.v2.copy(box.max); + const max = + vertex2.x - vertex1.x > vertex2.y - vertex1.y + ? vertex2.x - vertex1.x + : vertex2.y - vertex1.y; + + if (!this._tempPositionMesh) { + this._tempPositionMesh = new THREE.Mesh( + new THREE.PlaneGeometry(max, max), + new THREE.MeshBasicMaterial() + ); + } else { + const currentSize = ( + this._tempPositionMesh.geometry as THREE.PlaneGeometry + ).parameters.width; + if (Math.abs(currentSize - max) > 0.01) { + this._tempPositionMesh.geometry.dispose(); + this._tempPositionMesh.geometry = new THREE.PlaneGeometry(max, max); + } + } + + const invMesh = this._tempPositionMesh; + invMesh.position.copy(this.carpetMesh.position); + invMesh.rotation.copy(this.carpetMesh.rotation); + + if (this.carpetRatio.w < 1.25) { + invMesh.scale.y = this.carpetMesh.scale.x / 0.8; + invMesh.scale.x = this.carpetMesh.scale.x; + } else { + invMesh.scale.x = this.carpetMesh.scale.y * 0.8; + invMesh.scale.y = this.carpetMesh.scale.y; + } + + invMesh.updateMatrix(); + invMesh.updateMatrixWorld(); + + const positionAttr = invMesh.geometry.attributes.position; + const vector = this._vector3Pool.v3; + const size = this._vector2Pool.v1; + this.renderer.getSize(size); + + const temparray: {x: number; y: number}[] = []; + for (let i = 0, l = positionAttr.count; i < l; i++) { + vector.fromBufferAttribute(positionAttr, i); + vector.applyMatrix4(invMesh.matrixWorld); + const canvasVertex = createVector( + vector, + this.camera, + size.width, + size.height + ); + temparray.push({ + x: this.getResizedImageCoordinatesX(canvasVertex.x), + y: this.getResizedImageCoordinatesY(canvasVertex.y), + }); + } + this.myRoomPointsOBJ.carpetpoints = temparray; + } + + updateGizmo(options: {show?: boolean} = {}) { + const {show = true} = options; + if (!this.gizmoCanvas || !this.carpetMesh) return; + const context = this.gizmoCanvas.getContext('2d')!; + const {width, height} = this.gizmoCanvas; + + if (!show) { + clearCanvas(this.gizmoCanvas, width, height); + return; + } + + const carpetSize = this.calculateCarpetSize(); + const smallerDim = + carpetSize.x > carpetSize.y ? carpetSize.x : carpetSize.y; + const carpetRadius = smallerDim / 5; + const carpetCenter = this.carpetMesh.position.clone(); + + const dist1 = this.distbetween2Vertices( + carpetCenter.clone(), + new THREE.Vector3( + carpetCenter.x, + carpetCenter.y, + carpetCenter.z + carpetRadius + ) + ); + const dist2 = this.distbetween2Vertices( + new THREE.Vector3( + carpetCenter.x + carpetRadius, + carpetCenter.y, + carpetCenter.z + ), + carpetCenter.clone() + ); + + const area1 = areaOfellipse(dist1.yDist, dist2.xDist); + const area2 = areaOfellipse(dist2.yDist, dist1.xDist); + const radX = area1 > area2 ? dist2.xDist : dist2.yDist; + const radY = area1 > area2 ? dist1.yDist : dist1.xDist; + + const radiusX = radX > radY ? radX : radY; + const radiusY = radX > radY ? radY : radX; + const diamondHeight = 10; + const canvasCenter = createVector(carpetCenter, this.camera, width, height); + const colorStr = 'rgba(250, 250, 250, 0.8)'; + + context.strokeStyle = colorStr; + context.fillStyle = colorStr; + context.lineWidth = 1; + context.shadowOffsetX = 0; + context.shadowColor = 'black'; + context.shadowOffsetY = 1; + context.clearRect(0, 0, width, height); + context.beginPath(); + context.ellipse( + canvasCenter.x, + canvasCenter.y, + radiusX, + radiusY, + 0, + 0, + 2 * Math.PI + ); + context.stroke(); + context.beginPath(); + context.moveTo(canvasCenter.x, canvasCenter.y + radiusY - 5); + context.lineTo(canvasCenter.x + diamondHeight, canvasCenter.y + radiusY); + context.lineTo(canvasCenter.x, canvasCenter.y + radiusY + 5); + context.lineTo(canvasCenter.x - diamondHeight, canvasCenter.y + radiusY); + context.fill(); + } + + findGizmoIntersection(e: {x: number; y: number}): boolean { + if (!this.gizmoCanvas) return false; + const imgData = this.gizmoCanvas + .getContext('2d')! + .getImageData(e.x - 10, e.y - 10, 20, 20); + for (let i = 0; i < imgData.data.length; i += 4) { + if (imgData.data[i + 3] !== 0) return true; + } + return false; + } + + autoLevel(bgCanvas: HTMLCanvasElement, maskCanvas: HTMLCanvasElement) { + this.origBG = bgCanvas.toDataURL(); + const size = new THREE.Vector2(); + this.renderer.getSize(size); + const {x: width, y: height} = size; + + const canvas = createCanvas(width, height); + const ctx = canvas.getContext('2d')!; + ctx.drawImage(bgCanvas, 0, 0, width, height); + const imgData = ctx.getImageData(0, 0, width, height); + const pixelNum = imgData.data.length; + + let redMax = 0, + redMin = 255, + greenMax = 0, + greenMin = 255, + blueMax = 0, + blueMin = 255; + + for (let i = 0; i < pixelNum; i += 4) { + if (imgData.data[i] > redMax) redMax = imgData.data[i]; + if (imgData.data[i] < redMin) redMin = imgData.data[i]; + if (imgData.data[i + 1] > greenMax) greenMax = imgData.data[i + 1]; + if (imgData.data[i + 1] < greenMin) greenMin = imgData.data[i + 1]; + if (imgData.data[i + 2] > blueMax) blueMax = imgData.data[i + 2]; + if (imgData.data[i + 2] < blueMin) blueMin = imgData.data[i + 2]; + } + + for (let j = 0; j < pixelNum; j += 4) { + imgData.data[j] = (imgData.data[j] - redMin) * (255 / (redMax - redMin)); + imgData.data[j + 1] = + (imgData.data[j + 1] - greenMin) * (255 / (greenMax - greenMin)); + imgData.data[j + 2] = + (imgData.data[j + 2] - blueMin) * (255 / (blueMax - blueMin)); + } + ctx.putImageData(imgData, 0, 0); + bgCanvas.getContext('2d')!.drawImage(canvas, 0, 0); + const imgDataCopy = imgData.valueOf() as ImageData; + + const maskCanvas1 = createCanvas(width, height); + const maskContext = maskCanvas1.getContext('2d')!; + readImage(this.roomdataurl).then((image) => { + maskContext.drawImage(image, 0, 0, width, height); + const maskData = maskContext.getImageData(0, 0, width, height); + for (let k = 0; k < maskData.data.length; k += 4) { + imgDataCopy.data[k + 3] = maskData.data[k + 3]; + } + maskContext.putImageData(imgDataCopy, 0, 0); + maskCanvas.width = width; + maskCanvas.height = height; + maskCanvas.getContext('2d')!.drawImage(maskCanvas1, 0, 0, width, height); + }); + } + + undoAutoLevel(bgCanvas: HTMLCanvasElement, maskCanvas: HTMLCanvasElement) { + readImage(this.origBG).then((origImage) => { + bgCanvas.getContext('2d')!.drawImage(origImage, 0, 0); + }); + + const size = new THREE.Vector2(); + this.renderer.getSize(size); + const {x: width, y: height} = size; + const maskContext = maskCanvas.getContext('2d')!; + readImage(this.roomdataurl).then((image) => { + maskCanvas.width = width; + maskCanvas.height = height; + clearCanvas(maskCanvas, width, height); + maskContext.drawImage(image, 0, 0, width, height); + }); + } + + markFF( + x: number, + y: number, + inputCanvas: HTMLCanvasElement, + inputSelected: string + ) { + if (inputSelected === 'eraser') { + this.removePoint(inputCanvas, x, y); + } else { + const markingColor = inputSelected === 'floor' ? '#FF0000' : '#00FF00'; + this.markWithColor(inputCanvas, markingColor, x, y); + inputSelected === 'floor' + ? this.pushFloorPoints(x, y) + : this.pushFurniPoints(x, y); + } + } + + markWithColor( + inputCanvas: HTMLCanvasElement, + MarkingColor: string, + x: number, + y: number + ) { + const ctx = inputCanvas.getContext('2d')!; + ctx.moveTo(x, y); + ctx.fillStyle = MarkingColor; + ctx.beginPath(); + ctx.arc(x, y, 2, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.closePath(); + } + + isLastPoint(pointsList: {x: number; y: number}[], ptX: number, ptY: number) { + if (!pointsList.length) return false; + const lastPt = pointsList[pointsList.length - 1]; + return lastPt.x === ptX && lastPt.y === ptY; + } + + pushFloorPoints(x: number, y: number) { + x = this.getResizedImageCoordinatesX(x); + y = this.getResizedImageCoordinatesY(y); + if (!this.isLastPoint(this.myRoomPointsOBJ.floorpoints, x, y)) { + this.myRoomPointsOBJ.floorpoints.push({x, y}); + } + } + + pushFurniPoints(x: number, y: number) { + x = this.getResizedImageCoordinatesX(x); + y = this.getResizedImageCoordinatesY(y); + if (!this.isLastPoint(this.myRoomPointsOBJ.notfloorpoints, x, y)) { + this.myRoomPointsOBJ.notfloorpoints.push({x, y}); + } + } + + removePoint(inputCanvas: HTMLCanvasElement, x: number, y: number) { + x = this.getResizedImageCoordinatesX(x); + y = this.getResizedImageCoordinatesY(y); + const isFarEnough = (point: {x: number; y: number}) => { + const a = x - point.x; + const b = y - point.y; + return Math.sqrt(a * a + b * b) >= 30; + }; + this.myRoomPointsOBJ.notfloorpoints = + this.myRoomPointsOBJ.notfloorpoints.filter(isFarEnough); + this.myRoomPointsOBJ.floorpoints = + this.myRoomPointsOBJ.floorpoints.filter(isFarEnough); + this.drawFFPoints(inputCanvas); + } + + drawFFPoints(inputCanvas: HTMLCanvasElement) { + inputCanvas + .getContext('2d')! + .clearRect(0, 0, inputCanvas.width, inputCanvas.height); + this.colorPoints(inputCanvas, this.myRoomPointsOBJ.floorpoints, '#FF0000'); + this.colorPoints( + inputCanvas, + this.myRoomPointsOBJ.notfloorpoints, + '#00FF00' + ); + } + + colorPoints( + inputCanvas: HTMLCanvasElement, + points: {x: number; y: number}[], + markingColor: string + ) { + for (const point of points) { + this.markWithColor( + inputCanvas, + markingColor, + this.getInputCanvasCoordinatesX(point.x), + this.getInputCanvasCoordinatesY(point.y) + ); + } + } + + updatePointHistory() { + this.pointsHistory.floorpoints = this.myRoomPointsOBJ.floorpoints; + this.pointsHistory.notfloorpoints = this.myRoomPointsOBJ.notfloorpoints; + this.pointsHistory.inputSelected = this.myroomInputSelected; + this.pointsHistoryArray.push(JSON.stringify(this.pointsHistory)); + } + + pointsHistoryReset() { + this.myRoomPointsOBJ.floorpoints = []; + this.myRoomPointsOBJ.notfloorpoints = []; + this.pointsHistoryArray = []; + } + + undoPoints(inputCanvas: HTMLCanvasElement) { + if (this.pointsHistoryArray.length === 0) return; + this.pointsHistoryArray.pop(); + if (this.pointsHistoryArray.length === 0) { + this.myRoomPointsOBJ.floorpoints = []; + this.myRoomPointsOBJ.notfloorpoints = []; + this.drawFFPoints(inputCanvas); + return; + } + this.pointsHistory = JSON.parse( + this.pointsHistoryArray[this.pointsHistoryArray.length - 1] + ); + this.myRoomPointsOBJ.floorpoints = this.pointsHistory.floorpoints; + this.myRoomPointsOBJ.notfloorpoints = this.pointsHistory.notfloorpoints; + this.drawFFPoints(inputCanvas); + } + + resetPoints(inputCanvas: HTMLCanvasElement) { + this.pointsHistoryReset(); + this.drawFFPoints(inputCanvas); + clearCanvas(inputCanvas, inputCanvas.width, inputCanvas.height); + } + + getResizedImageCoordinatesX(x: number) { + return Math.round(x * this.ratioWid); + } + + getResizedImageCoordinatesY(y: number) { + return Math.round(y * this.ratioHgt); + } + + getInputCanvasCoordinatesX(x: number) { + return Math.round(x / this.ratioWid); + } + + getInputCanvasCoordinatesY(y: number) { + return Math.round(y / this.ratioHgt); + } + + resize(windowSize: {width: number; height: number}) { + if (!this.canvasInitiated) return; + const {width, height} = resizeKeepingAspect(this.bgImage, windowSize); + + this.bgCanvas.style.width = `${width}px`; + this.bgCanvas.style.height = `${height}px`; + this.maskCanvas.style.width = `${width}px`; + this.maskCanvas.style.height = `${height}px`; + + this.inputCanvas.width = width; + this.inputCanvas.height = height; + this.gizmoCanvas.width = width; + this.gizmoCanvas.height = height; + this.resizeRenderer(width, height); + this.updateGizmo(); + } + + resizeRenderer(width: number, height: number) { + if (this.camera) { + this.camera.aspect = width / height; + this.camera.updateProjectionMatrix(); + } + this.renderer.setSize(width, height); + this.ratioWid = this.origWid / width; + this.ratioHgt = this.origHgt / height; + this.render(); + } + + calculateCarpetSize() { + if (!this.carpetMesh) return new THREE.Vector3(); + this.carpetMesh.geometry.computeBoundingBox(); + const carpetSize = new THREE.Vector3(); + this.carpetMesh.geometry.boundingBox!.getSize(carpetSize); + return carpetSize; + } + + calculateCameraWidth() { + const carpetSize = this.calculateCarpetSize(); + const siz = new THREE.Vector2(); + this.renderer.getSize(siz); + const carpetWid = carpetSize.x > carpetSize.y ? carpetSize.x : carpetSize.y; + return siz.x >= siz.y ? carpetWid * 1.1 : carpetWid * 1.2; + } + + distbetween2Vertices(vertex1: THREE.Vector3, vertex2: THREE.Vector3) { + const vec = new THREE.Vector2(); + this.renderer.getSize(vec); + const v1 = createVector(vertex1, this.camera, vec.x, vec.y); + const v2 = createVector(vertex2, this.camera, vec.x, vec.y); + return { + xDist: Math.abs(Math.abs(v2.x) - Math.abs(v1.x)), + yDist: Math.abs(Math.abs(v2.y) - Math.abs(v1.y)), + }; + } + + calculateVerticalHeight() { + const {x, y} = this.calculateCarpetSize(); + const h1 = this.distbetween2Vertices( + new THREE.Vector3(-x / 2, 0, 0), + new THREE.Vector3(x / 2, 0, 0) + ); + const h2 = this.distbetween2Vertices( + new THREE.Vector3(0, 0, y / 2), + new THREE.Vector3(0, 0, -y / 2) + ); + return h1.yDist > h2.yDist ? h1.yDist : h2.yDist; + } + + angleBetween2Vertices(vertex1: THREE.Vector3, vertex2: THREE.Vector3) { + const size = new THREE.Vector2(); + this.renderer.getSize(size); + const v1 = createVector(vertex1, this.camera, size.width, size.height); + const v2 = createVector(vertex2, this.camera, size.width, size.height); + return (Math.atan((v2.y - v1.y) / (v2.x - v1.x)) * 180) / Math.PI; + } + + changeCameraDist(width: number, fovDeg: number) { + const fovrad = (fovDeg * Math.PI) / 180; + const distance = width / (2 * Math.tan(fovrad / 2)); + const {camera, orbit} = this; + const cur_dist = Math.sqrt( + camera.position.x * camera.position.x + + camera.position.y * camera.position.y + + camera.position.z * camera.position.z + ); + const dist_ratio = distance / cur_dist; + orbit.object.position.set( + dist_ratio * camera.position.x, + dist_ratio * camera.position.y, + dist_ratio * camera.position.z + ); + orbit.update(); + this.render(); + } + + calculateAngle1() { + return this.angleBetween2Vertices( + new THREE.Vector3(0, 0, -500), + new THREE.Vector3(0, 0, 500) + ); + } + + calculateAngle2() { + return ( + 180 - + Math.abs( + this.angleBetween2Vertices( + new THREE.Vector3(400, 0, 0), + new THREE.Vector3(-400, 0, 0) + ) + ) + ); + } + + resetOrbit(x = 0, y = 0) { + this.orbit.reset(); + this.changeCameraDist(this.calculateCameraWidth(), this.camera.fov); + (this.orbit as any).pan(x, y); + this.orbit.update(); + } + + setinitialOrientation(res: AnalysisData) { + this.roomAnalysisData = res; + + if (this.carpetMesh && this.fbxLoaded) { + const newRotation = this.calculateCarpetRotation(res); + this.carpetMesh.rotation.fromArray(convertArrIntoRad(newRotation)); + this.initCarpetRot = newRotation; + const newPosition = this.calculateCarpetPosition(res); + this.carpetMesh.position.fromArray(newPosition); + this.initCarpetPos = newPosition; + this.render(); + } + + this.orbit.reset(); + this.camera.position.set(0, 0, 720); + this.orbit.update(); + + let fov = parseFloat(res.fov); + if (fov < 30) fov = 30; + if (fov > 60) fov = 60; + + this.camera.fov = fov; + this.camera.updateProjectionMatrix(); + this.render(); + this.changeCameraDist(this.calculateCameraWidth(), fov); + + let horAngleForCamera = parseFloat(res.orientation.hor); + const horAngleSnapTolerance = 10; + if (Math.abs(horAngleForCamera) < horAngleSnapTolerance) + horAngleForCamera = 0; + else if (Math.abs(horAngleForCamera - 90) < horAngleSnapTolerance) + horAngleForCamera = 90; + else if (Math.abs(horAngleForCamera + 90) < horAngleSnapTolerance) + horAngleForCamera = -90; + + (this.orbit as any).setAzimuthalAngle( + -((90 - horAngleForCamera) * Math.PI) / 180 + ); + this.orbit.update(); + (this.orbit as any).setPolarAngle((10 * Math.PI) / 180); + this.orbit.update(); + (this.orbit as any).pan( + parseFloat(res.floor_center.x), + parseFloat(res.floor_center.y) + ); + this.orbit.update(); + + const angle1pred = Math.abs(parseFloat(res.orientation.hor)); + const angle2pred = Math.abs(parseFloat(res.orientation.vert)); + const global_iter_limit = 100; + const iter_limit = 50; + let index = 0; + + const calcError = (predicted: number, actual: number) => + (Math.abs(Math.abs(predicted) - actual) / actual) * 100; + + if (res.orientation.exception) { + if (!res.orientation.flag) { + this.orbit.rotateLeft(Math.PI / 2); + this.orbit.update(); + } + } else { + let vertical_error = calcError(this.calculateAngle2(), angle2pred); + let horizontal_error = calcError(this.calculateAngle1(), angle1pred); + let hor_angle_per_step = 1; + let vert_angle_per_step = 1; + const decay_rate = 0.5; + + const optimizeVertically = (step: number) => { + let angle2 = Math.abs(this.calculateAngle2()); + const vert_angle = angle2 < 90 ? -step : step; + let i1 = 0, + i2 = 0; + while (angle2 > angle2pred) { + this.orbit.rotateUp(THREE.MathUtils.degToRad(vert_angle)); + this.orbit.update(); + angle2 = Math.abs(this.calculateAngle2()); + if (++i1 === iter_limit) break; + } + while (angle2 < angle2pred) { + this.orbit.rotateUp(THREE.MathUtils.degToRad(-vert_angle)); + this.orbit.update(); + angle2 = Math.abs(this.calculateAngle2()); + if (++i2 === iter_limit) break; + } + }; + + const optimizeHorizontally = (step: number) => { + let angle1 = Math.abs(this.calculateAngle1()); + let i1 = 0, + i2 = 0; + while (angle1 < angle1pred) { + this.orbit.rotateLeft(THREE.MathUtils.degToRad(step)); + this.orbit.update(); + angle1 = Math.abs(this.calculateAngle1()); + if (++i1 === iter_limit) break; + } + while (angle1 > angle1pred) { + this.orbit.rotateLeft(THREE.MathUtils.degToRad(-step)); + this.orbit.update(); + angle1 = Math.abs(this.calculateAngle1()); + if (++i2 === iter_limit) break; + } + }; + + while (vertical_error > 0.1 || horizontal_error > 2) { + optimizeHorizontally(hor_angle_per_step); + optimizeVertically(vert_angle_per_step); + vertical_error = calcError(this.calculateAngle2(), angle2pred); + horizontal_error = calcError(this.calculateAngle1(), angle1pred); + hor_angle_per_step *= decay_rate; + vert_angle_per_step *= decay_rate; + if (++index === global_iter_limit) break; + } + + this.orbit.update(); + if (!res.orientation.flag) { + this.orbit.rotateLeft(Math.PI / 2); + this.orbit.update(); + } + + const size = new THREE.Vector2(); + this.renderer.getSize(size); + let verticalHeight = this.calculateVerticalHeight(); + while (verticalHeight < 0.15 * size.height) { + this.orbit.rotateUp((0.5 * Math.PI) / 180); + this.orbit.update(); + verticalHeight = this.calculateVerticalHeight(); + } + this.orbit.update(); + } + + this.render(); + this.changeCameraDist(this.calculateCameraWidth(), fov); + this.render(); + } + + removemeshes() { + if (!this.carpetMesh) return; + (this.carpetMesh.material as THREE.MeshBasicMaterial).opacity = 1; + this.render(); + } + + saveAsRoom({ + mode, + roomId, + file, + props, + }: { + mode: string; + roomId: string; + file: unknown; + props: unknown; + }) { + this.getCarpetPositions(); + const {carpetpoints, floorpoints, notfloorpoints} = this.myRoomPointsOBJ; + return AppProvider.saveAsRoom({ + mode, + roomId, + file, + props, + floorpoints, + notfloorpoints, + carpetpoints, + }).then((response) => ({response})); + } + + downloadImage(downloadBLob: Blob, filename: string) { + return new Promise((resolve) => { + const url = window.URL || (window as any).webkitURL; + const imageSrc = url.createObjectURL(downloadBLob); + const link = document.createElement('a'); + document.body.appendChild(link); + link.setAttribute('download', filename); + link.href = imageSrc; + if ((navigator as any).msSaveOrOpenBlob) { + (navigator as any).msSaveOrOpenBlob(downloadBLob, filename); + } else { + link.click(); + } + document.body.removeChild(link); + resolve(imageSrc); + }); + } + + saveAsImage = async ({ + bgCanvas, + maskCanvas, + designPath, + }: { + bgCanvas: HTMLCanvasElement; + maskCanvas: HTMLCanvasElement; + designPath: string; + }) => { + if (!this.carpetMesh) return; + (this.carpetMesh.material as THREE.MeshBasicMaterial).opacity = 1; + this.removemeshes(); + const imgData = this.renderer.domElement.toDataURL('image/png'); + + readImage(imgData).then((rendererImage) => { + const downloadCanvas = createCanvas( + rendererImage.width, + rendererImage.height + ); + const downloadContext = downloadCanvas.getContext('2d')!; + downloadContext.drawImage( + bgCanvas, + 0, + 0, + rendererImage.width, + rendererImage.height + ); + downloadContext.drawImage( + rendererImage, + 0, + 0, + rendererImage.width, + rendererImage.height + ); + downloadContext.drawImage( + maskCanvas, + 0, + 0, + rendererImage.width, + rendererImage.height + ); + + const win = window as unknown as WindowExtended; + if (win.flags.inhouseChanges?.saveFunctionForFlutter) { + win.getByteData = downloadCanvas.toDataURL('image/jpeg', 0.95); + return; + } + + canvasToBlobPromise(downloadCanvas).then((downloadBLob) => { + let name = 'customdesign'; + if (!win.initialData?.customDesignUrl) { + const filename = designPath.split('/').pop()!; + name = filename.split('.')[0]; + } + this.downloadImage(downloadBLob, name + '-myroom.jpg').then(() => { + if (this.carpetMesh) { + (this.carpetMesh.material as THREE.MeshBasicMaterial).opacity = 1; + } + this.render(); + }); + }); + }); + }; + + dispose() { + if (this.renderer) { + this.renderer.dispose(); + this.renderer.forceContextLoss(); + this.renderer = null!; + } + if (this.orbit) { + this.orbit.dispose(); + this.orbit = null!; + } + if (this.carpetMesh) { + this.carpetMesh.geometry?.dispose(); + const mat = this.carpetMesh.material as THREE.MeshBasicMaterial; + mat.map?.dispose(); + mat.dispose(); + this.carpetMesh = undefined; + } + if (this._tempPositionMesh) { + this._tempPositionMesh.geometry?.dispose(); + (this._tempPositionMesh.material as THREE.Material).dispose(); + this._tempPositionMesh = undefined; + } + if (this.scene) { + this.scene.clear(); + this.scene = null!; + } + this.camera = null!; + this.textureLoader = null!; + this.roomAnalysisData = null; + this._cachedFloorBounds = null; + this._cachedCarpetDims = null; + } +} diff --git a/src/Components/RoomView/helpers/ThreeViewHelper.ts b/src/Components/RoomView/helpers/ThreeViewHelper.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/RoomView/index.ts b/src/Components/RoomView/index.ts index a533500..bd0fc98 100644 --- a/src/Components/RoomView/index.ts +++ b/src/Components/RoomView/index.ts @@ -1,20 +1,168 @@ -import {LitElement, html, css} from 'lit'; -import {customElement, property} from 'lit/decorators.js'; +// src/room-illustration.ts + +import {LitElement, html, css} from 'lit'; +import {customElement, property, query, state} from 'lit/decorators.js'; +import {RoomViewHelper} from './helpers/RoomViewHelper'; +import type { + RoomData, + DesignDetails, + CarpetOptions, + AvailableSize, +} from './types'; + +let roomViewHelper = new RoomViewHelper(); + +@customElement('room-illustration') +export class RoomIllustration extends LitElement { + // ─── Props in (replaces all useXxxState() hook reads) ──────────────── + @property({type: Object}) roomData: RoomData | null = null; + @property({type: Object}) designDetails: DesignDetails | null = null; + @property({type: Object}) carpetOptions: CarpetOptions | null = null; + @property({type: Object}) activeColor: {Color: string} | null = null; + @property({type: Object}) selectedSize: AvailableSize | null = null; + @property({type: Boolean}) isIdle = false; + + // ─── Internal state (replaces useState) ────────────────────────────── + @state() private shotReady = false; + @state() private transition = -1; + + // ─── Canvas refs (replaces useRef) ─────────────────────────────────── + @query('.container') private containerEl!: HTMLDivElement; + @query('.bg-canvas') private bgCanvas!: HTMLCanvasElement; + @query('.three-canvas') private threeCanvas!: HTMLCanvasElement; + @query('.mask-canvas') private maskCanvas!: HTMLCanvasElement; + @query('.shadow-canvas') private shadowCanvas!: HTMLCanvasElement; + @query('.input-canvas') private inputCanvas!: HTMLCanvasElement; + @query('.obj-container') private objContainer!: HTMLDivElement; + + // ─── Lifecycle ──────────────────────────────────────────────────────── + + override firstUpdated() { + // Equivalent to useMount in React — canvases are in the DOM + // roomViewHelper.initCanvas({ + // bgCanvas: this.bgCanvas, + // threeCanvas: this.threeCanvas, + // maskCanvas: this.maskCanvas, + // shadowCanvas: this.shadowCanvas, + // container: this.containerEl, + // inputCanvas: this.inputCanvas, + // objCanvasContainer: this.objContainer, + // }); + + window.addEventListener('resize', () => this.sizeContainers()); + } + + override updated(changed: Map) { + // if (changed.has('roomData') && this.roomData) { + // this.loadRoom(); + // } + // if (changed.has('carpetOptions') && this.carpetOptions && this.shotReady) { + // this.updateCarpet(); + // } + // if (changed.has('activeColor') && this.activeColor && this.shotReady) { + // roomViewHelper.updateBackground({ + // dominantColorHex: this.activeColor.Color, + // }); + // roomViewHelper.updateMask(); + // roomViewHelper.updateShadow(); + // } + // if (changed.has('selectedSize') && this.selectedSize) { + // roomViewHelper.change3dObjectScale(this.selectedSize); + // } + } + + override disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener('resize', () => this.sizeContainers()); + // roomViewHelper.clearAll(); + } + + // ─── Internal methods (replaces inline useEffect logic) ────────────── + + private async loadRoom() { + // if (!this.roomData) return; + // const {config, baseUrl, files, shot} = this.roomData; + // roomViewHelper.clearAll(); + // roomViewHelper.initConfig({baseUrl, config, files}); + // this.sizeContainers(); + // await this.initShot({shot: shot ?? config.defaultShot}); + } + + private async initShot(options: {shot: string}) { + // mirrors initShot logic from IllustrationView/index.jsx + // ... + this.shotReady = true; + this.emitRenderingComplete(); + } + + private async updateCarpet() { + if (!this.carpetOptions) return; + // await roomViewHelper.updateCarpetRotation(this.carpetOptions.rotation); + // await roomViewHelper.updateCarpetPosition(this.carpetOptions.position); + // await roomViewHelper.updateShadow(); + } + + private sizeContainers() { + // resize canvas to container size + } + + // ─── Events out (replaces callback props) ──────────────────────────── + + private emitRenderingComplete() { + this.dispatchEvent( + new CustomEvent('rendering-complete', {bubbles: true, composed: true}) + ); + } + + private emitSizesChange(sizes: AvailableSize[]) { + this.dispatchEvent( + new CustomEvent('sizes-change', { + detail: sizes, + bubbles: true, + composed: true, + }) + ); + } + + private emitActiveClick() { + this.dispatchEvent( + new CustomEvent('active-click', {bubbles: true, composed: true}) + ); + } + + // ─── Template (replaces JSX canvas layout) ─────────────────────────── -@customElement('room-view') -export class RoomView extends LitElement { static override styles = css` :host { + display: block; + position: relative; + width: 100%; + height: 100%; + } + .container { + position: relative; + width: 100%; + height: 100%; + } + canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } `; override render() { - return html`

RoomView

`; - } -} - -declare global { - interface HTMLElementTagNameMap { - 'room-view': RoomView; + return html` +
+ + + + + +
+
+ `; } } diff --git a/src/Components/RoomView/roomView-react.ts b/src/Components/RoomView/roomView-react.ts deleted file mode 100644 index 1678459..0000000 --- a/src/Components/RoomView/roomView-react.ts +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import {createComponent} from '@lit/react'; -import {RoomView} from './index'; -export const RoomViewReact = createComponent({ - tagName: 'room-view', - elementClass: RoomView, - react: React, -}); diff --git a/src/Components/RoomView/types.ts b/src/Components/RoomView/types.ts new file mode 100644 index 0000000..ad37dfa --- /dev/null +++ b/src/Components/RoomView/types.ts @@ -0,0 +1,75 @@ +// src/types.ts +import * as THREE from 'three'; +export interface RoomData { + Name: string; + Files: string[]; + Dir: string; + baseUrl: string; + config: RoomConfig; + shot?: string; +} + +export interface RoomConfig { + version: number; + roomType: 'roomview' | 'illustration' | 'photographic' | 'perspective'; + dims: {width: number; height: number}; + name: string; + hasShadow: boolean; + mouseControls: boolean; + realTimeDynamicRendering: boolean; + roomElements: Record; + roomAssets: Record; + scene1: {surface1: {rotation: number[]; position: number[]}}; +} + +export interface DesignDetails { + hash: string; + fullpath: string; + designDetails: Record; + updateDesignTiles?: boolean; + updateNormapTiles?: boolean; +} + +export interface CarpetOptions { + rotation: [number, number, number]; + position: [number, number, number]; +} + +export interface AvailableSize { + width: number; + height: number; + unit: string; +} +export interface MyRoomPointsOBJ { + floorpoints: {x: number; y: number}[]; + notfloorpoints: {x: number; y: number}[]; + carpetpoints: {x: number; y: number}[]; +} + +export interface PointsHistory { + floorpoints: {x: number; y: number}[]; + notfloorpoints: {x: number; y: number}[]; + inputSelected: string; +} + +export interface CarpetRatio { + w: number; + h: number; +} + +export interface ScaleFactor { + x: number; + y: number; +} + +export interface Vector3Pool { + v1: THREE.Vector3; + v2: THREE.Vector3; + v3: THREE.Vector3; + v4: THREE.Vector3; +} + +export interface Vector2Pool { + v1: THREE.Vector2; + v2: THREE.Vector2; +} diff --git a/src/Imports/exploRUG.ts b/src/Imports/exploRUG.ts index cffc52e..a7cceba 100644 --- a/src/Imports/exploRUG.ts +++ b/src/Imports/exploRUG.ts @@ -1,2 +1,2 @@ -import '../Components/RoomView'; -import '../Components/RoomView/roomView-react.ts'; +import '../Components/RoomView/index.js'; +import '../react-wrapper/roomView-react.js'; diff --git a/src/Utils/canvasutils.ts b/src/Utils/canvasutils.ts new file mode 100644 index 0000000..33f6617 --- /dev/null +++ b/src/Utils/canvasutils.ts @@ -0,0 +1,317 @@ +import {readImage} from './domUtils'; + +declare const AppProvider: { + uploadRoomviewBlob: (params: {blob: Blob}) => Promise; + postLogDetails: (params: {actionid: string; filename: string}) => void; +}; + +interface WindowExtended extends Window { + flutter_inappwebview?: {callHandler: (name: string, data: string) => void}; + isNativeApp?: boolean; + flags: {inhouseChanges: {saveFunctionForFlutter?: boolean}}; + getByteData?: string; +} + +export const createCanvas = (w: number, h: number): HTMLCanvasElement => { + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + return canvas; +}; + +export const clearCanvas = ( + canvas: HTMLCanvasElement, + w: number, + h: number +) => { + canvas.getContext('2d')?.clearRect(0, 0, w, h); +}; + +export function applyEdgeFilter( + canvas: HTMLCanvasElement, + image: HTMLImageElement +): HTMLCanvasElement { + const pix = 4; + const blur = '2px'; + const brightness = '50%'; + const {width: w, height: h} = canvas; + + const designCtx = canvas.getContext('2d')!; + const filterCanvas = createCanvas(w, h); + const designfilterCtx = filterCanvas.getContext('2d')!; + designfilterCtx.filter = `blur(${blur}) brightness(${brightness})`; + designfilterCtx.drawImage(image, 0, 0, w, h); + + designCtx.putImageData(designfilterCtx.getImageData(0, 0, pix, h), 0, 0); + designCtx.putImageData(designfilterCtx.getImageData(0, 0, w, pix), 0, 0); + designCtx.putImageData( + designfilterCtx.getImageData(0, h - pix, w, pix), + 0, + h - pix + ); + designCtx.putImageData( + designfilterCtx.getImageData(w - pix, 0, pix, h), + w - pix, + 0 + ); + return canvas; +} + +export function applyBWMask( + maskCanvas: HTMLCanvasElement, + imageUrl: string, + maskUrl: string, + callback: (canvas: HTMLCanvasElement) => void +): void { + const {width, height} = maskCanvas; + const tempCanvas = createCanvas(width, height); + const maskCtx = maskCanvas.getContext('2d')!; + const tempCtx = tempCanvas.getContext('2d')!; + + const img = new Image(); + img.src = imageUrl; + img.crossOrigin = 'Anonymous'; + img.onload = () => { + maskCanvas.width = width; + maskCanvas.height = height; + maskCtx.drawImage(img, 0, 0, width, height); + + const maskImg = new Image(); + maskImg.src = maskUrl; + maskImg.crossOrigin = 'Anonymous'; + maskImg.onload = () => { + tempCanvas.width = width; + tempCanvas.height = height; + tempCtx.drawImage(maskImg, 0, 0, width, height); + + const imgData = maskCtx.getImageData(0, 0, width, height); + const maskData = tempCtx.getImageData(0, 0, width, height); + for (let i = 0; i < maskData.data.length; i += 4) { + imgData.data[i + 3] = 255 - maskData.data[i]; + } + maskCtx.putImageData(imgData, 0, 0); + callback(maskCanvas); + }; + }; +} + +export const applyMask = ( + canvas: HTMLCanvasElement, + imgUrl: string, + maskUrl: string +): Promise => { + const {width, height} = canvas; + const tempCanvas = createCanvas(width, height); + const ctx = canvas.getContext('2d')!; + const tempCtx = tempCanvas.getContext('2d')!; + + return Promise.all([readImage(imgUrl), readImage(maskUrl)]).then( + ([image, mask]) => { + ctx.drawImage(image, 0, 0, width, height); + tempCtx.drawImage(mask, 0, 0, width, height); + + const imgData = ctx.getImageData(0, 0, width, height); + const maskData = tempCtx.getImageData(0, 0, width, height); + for (let i = 0; i < maskData.data.length; i += 4) { + imgData.data[i + 3] = 255 - maskData.data[i]; + } + ctx.putImageData(imgData, 0, 0); + } + ); +}; + +export const bnwToTransparency = ( + canvas: HTMLCanvasElement, + bnwUrl: string, + amount: number +): void => { + const {width, height} = canvas; + const ctx = canvas.getContext('2d')!; + + const image = new Image(); + image.src = bnwUrl; + image.crossOrigin = 'Anonymous'; + image.onload = function () { + ctx.drawImage(image, 0, 0, width, height); + const imageData = ctx.getImageData(0, 0, width, height); + for (let i = 0; i < imageData.data.length; i += 4) { + imageData.data[i + 3] = (255 - imageData.data[i]) * amount; + imageData.data[i] = 0; + imageData.data[i + 1] = 0; + imageData.data[i + 2] = 0; + } + ctx.putImageData(imageData, 0, 0); + }; +}; + +export const resizeCanvas = ( + canvas: HTMLCanvasElement, + width: number, + height: number +): void => { + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; +}; + +export const downloadImageData = ( + canvas: HTMLCanvasElement, + name: string, + mime: string +): void => { + const mimetype = mime === 'jpg' ? 'jpeg' : mime; + const win = window as unknown as WindowExtended; + + const downloadBlob = (blob: Blob) => { + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.setAttribute('download', name); + const imageFileName = name.substring(0, name.lastIndexOf('.')); + + if (win.flutter_inappwebview || win.isNativeApp) { + AppProvider.uploadRoomviewBlob({blob}).then((response) => { + win.flutter_inappwebview!.callHandler( + 'downloadImage', + JSON.stringify({image: response, fileName: imageFileName, type: mime}) + ); + }); + } else { + setTimeout(() => { + if ((navigator as any).msSaveOrOpenBlob) { + (navigator as any).msSaveOrOpenBlob(blob, name); + } else { + anchor.click(); + } + }, 100); + } + AppProvider.postLogDetails({actionid: 'saveButtonClicked', filename: name}); + }; + + const type = `image/${mimetype}`; + if (win.flags.inhouseChanges.saveFunctionForFlutter) { + win.getByteData = canvas.toDataURL(type, 0.95); + return; + } + if (canvas.toBlob) { + canvas.toBlob( + (blob) => { + if (blob) { + downloadBlob(blob); + } + }, + type, + 0.95 + ); + return; + } + downloadBlob(dataURItoBlob(canvas.toDataURL(type, 0.95))); +}; + +export const canvasToBlobPromise = ( + canvas: HTMLCanvasElement, + imageQuality = 0.75 +): Promise => + new Promise((resolve) => { + if (canvas.toBlob) { + canvas.toBlob((blob) => { + if (blob) resolve(blob); + }); + return; + } + if ((canvas as any).msToBlob) { + resolve((canvas as any).msToBlob()); + return; + } + resolve(dataURItoBlob(canvas.toDataURL('image/jpeg', imageQuality))); + }); + +export const dataURItoBlob = (dataURI: string): Blob => { + const byteString = atob(dataURI.split(',')[1]); + const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + return new Blob([ab], {type: mimeString}); +}; + +export const dataURLtoBlobURL = (dataurl: string): Promise => { + return new Promise((resolve) => { + resolve(URL.createObjectURL(dataURItoBlob(dataurl))); + }); +}; + +export const cropStitchCanvas = ({ + origCanvas, + canvas, +}: { + origCanvas: HTMLCanvasElement; + canvas: HTMLCanvasElement; +}): void => { + const {width: canvasWidth, height: canvasHeight} = origCanvas; + const origCtx = origCanvas.getContext('2d')!; + const {width, height} = canvas; + const ctx = canvas.getContext('2d')!; + + if (width === canvasWidth && height === canvasHeight) { + ctx.drawImage(origCanvas, 0, 0); + return; + } + + const overlapSize = 20; + const overlapsx = canvasWidth - width / 2 - overlapSize; + const overlapsy = canvasHeight - height / 2 - overlapSize; + const wid = width / 2; + const hgt = height / 2; + + const tl = origCtx.getImageData(0, 0, wid, hgt); + const tr = origCtx.getImageData(overlapsx, 0, wid + overlapSize, hgt); + const bl = origCtx.getImageData(0, overlapsy, wid, hgt + overlapSize); + const br = origCtx.getImageData( + overlapsx, + overlapsy, + wid + overlapSize, + hgt + overlapSize + ); + + const trCanvas = createCanvas(tr.width, tr.height); + const trCtx = trCanvas.getContext('2d')!; + trCtx.putImageData(tr, 0, 0); + trCtx.globalCompositeOperation = 'destination-out'; + const trgrd = trCtx.createLinearGradient(0, 0, overlapSize, 0); + trgrd.addColorStop(0, 'black'); + trgrd.addColorStop(1, 'transparent'); + trCtx.fillStyle = trgrd; + trCtx.fillRect(0, 0, overlapSize, tr.height); + + const blCanvas = createCanvas(bl.width, bl.height); + const blCt = blCanvas.getContext('2d')!; + blCt.putImageData(bl, 0, 0); + blCt.globalCompositeOperation = 'destination-out'; + const blgrd = blCt.createLinearGradient(0, 0, 0, overlapSize); + blgrd.addColorStop(0, 'black'); + blgrd.addColorStop(1, 'transparent'); + blCt.fillStyle = blgrd; + blCt.fillRect(0, 0, bl.width, overlapSize); + + const brCanvas = createCanvas(br.width, br.height); + const brCt = brCanvas.getContext('2d')!; + brCt.putImageData(br, 0, 0); + brCt.globalCompositeOperation = 'destination-out'; + const brgrd = brCt.createLinearGradient(0, 0, 0, overlapSize); + brgrd.addColorStop(0, 'black'); + brgrd.addColorStop(1, 'transparent'); + brCt.fillStyle = brgrd; + brCt.fillRect(0, 0, br.width, overlapSize); + const brgrd1 = brCt.createLinearGradient(0, 0, overlapSize, 0); + brgrd1.addColorStop(0, 'black'); + brgrd1.addColorStop(1, 'transparent'); + brCt.fillStyle = brgrd1; + brCt.fillRect(0, 0, overlapSize, bl.height); + + ctx.putImageData(tl, 0, 0); + ctx.drawImage(trCanvas, width - tr.width, 0); + ctx.drawImage(blCanvas, 0, height - bl.height); + ctx.drawImage(brCanvas, width - br.width, height - br.height); +}; diff --git a/src/Utils/domUtils.ts b/src/Utils/domUtils.ts new file mode 100644 index 0000000..09cd1bb --- /dev/null +++ b/src/Utils/domUtils.ts @@ -0,0 +1,264 @@ +import {createCanvas} from './canvasutils'; +import {xoroshiro128plus} from 'pure-rand/generator/xoroshiro128plus'; +import {uniformInt} from 'pure-rand/distribution/uniformInt'; + +declare const AppProvider: { + domain: string; +}; + +export const downloadAsJSON = (object: unknown, name: string): void => { + const dataStr = + 'data:text/json;charset=utf-8,' + + encodeURIComponent(JSON.stringify(object)); + const dlAnchorElem = document.createElement('a'); + dlAnchorElem.setAttribute('href', dataStr); + dlAnchorElem.setAttribute('download', `${name}.json`); + dlAnchorElem.click(); +}; + +export const loadCssFile = ({cssUrl}: {cssUrl: string}): void => { + const link = document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('type', 'text/css'); + link.setAttribute('href', cssUrl); + document.getElementsByTagName('head')[0].appendChild(link); +}; + +export function readImage(url: string | Blob): Promise; +export function readImage>( + url: string | Blob, + i: T +): Promise<{image: HTMLImageElement} & T>; +export function readImage( + url: string | Blob, + i?: Record +): Promise< + HTMLImageElement | ({image: HTMLImageElement} & Record) +> { + const imageUrl = url instanceof Blob ? URL.createObjectURL(url) : url; + return new Promise((resolve, reject) => { + const image = new Image(); + image.crossOrigin = 'Anonymous'; + image.src = imageUrl; + image.onload = () => { + if (i) resolve({image, ...i}); + else resolve(image); + }; + image.onerror = reject; + }); +} + +export function scrollIntoViewIfNeeded(target: HTMLElement): void { + const rect = target.getBoundingClientRect(); + if (rect.bottom > window.innerHeight) target.scrollIntoView(false); + if (rect.top < 0) target.scrollIntoView(); +} + +export const isIE = (): boolean => + window.navigator.userAgent.indexOf('MSIE ') > 0 || + !!navigator.userAgent.match(/Trident.*rv:11\./); + +export function getJsonFromUrl(url?: string): Record { + if (!url) url = window.location.search; + const query = url.substring(1); + const result: Record = {}; + query.split('&').forEach((part) => { + const item = part.split('='); + result[item[0]] = decodeURIComponent(item[1]); + }); + return result; +} + +export const getFromSessionStorage = (key: string): unknown => { + const item = sessionStorage.getItem(key); + if (item === null || item === 'undefined') return undefined; + if (item === 'null') return null; + try { + return JSON.parse(item); + } catch { + return item; + } +}; + +export const getCustomClass = (): string => { + return (getFromSessionStorage('customclass') as string) || ''; +}; + +export const overlayTextureinCanvas = ({ + image, + color, + canvasSize, + canvas, +}: { + image: CanvasImageSource; + color: string; + canvasSize: number; + canvas: HTMLCanvasElement; +}): void => { + const ctx = canvas.getContext('2d')!; + const seed = [...color].reduce((acc, c) => (acc * 31 + c.charCodeAt(0)) | 0, 0); + const start = uniformInt(xoroshiro128plus(seed), 0, 256 - canvasSize); + const can = createCanvas( + (image as HTMLImageElement).width, + (image as HTMLImageElement).height + ); + can.getContext('2d')!.drawImage(image, 0, 0); + const imgData = can + .getContext('2d')! + .getImageData(start, start, canvasSize, canvasSize); + ctx.globalCompositeOperation = 'overlay'; + can.width = canvasSize; + can.height = canvasSize; + can.getContext('2d')!.putImageData(imgData, 0, 0); + ctx.drawImage(can, 0, 0); +}; + +export const copyToClipboard = (str: string): void => { + const el = document.createElement('textarea'); + el.value = str; + el.setAttribute('readonly', ''); + el.style.position = 'absolute'; + el.style.left = '-9999px'; + document.body.appendChild(el); + const selection = document.getSelection(); + const selected = + selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : false; + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + if (selected && selection) { + selection.removeAllRanges(); + selection.addRange(selected); + } +}; + +const stopPrntScr = (): void => { + const inpFld = document.createElement('input'); + inpFld.setAttribute('value', '.'); + inpFld.setAttribute('width', '0'); + inpFld.style.height = '0px'; + inpFld.style.width = '0px'; + inpFld.style.border = '0px'; + document.body.appendChild(inpFld); + inpFld.select(); + document.execCommand('copy'); + inpFld.remove(); +}; + +const accessClipboardData = (): void => { + try { + (window as any).clipboardData.setData('text', 'Access Restricted'); + } catch {} +}; + +export const disableScreenshot = (): void => { + document.addEventListener('keyup', (e) => { + if (e.key === 'PrintScreen') stopPrntScr(); + }); + accessClipboardData(); +}; + +export const inIframe = (): boolean => { + try { + return window.self !== window.top; + } catch { + return true; + } +}; + +interface SwipeData { + dir: string; + initial: number[]; + velocity: number; +} + +export const handleSwipeOnEdges = ( + data: SwipeData | null | undefined +): {left?: boolean; right?: boolean} | undefined => { + if (!data) return; + const {dir: direction, initial, velocity} = data; + const startPos = initial[0]; + if (velocity < 0.5) return; + + const detectableArea = + window.innerWidth < 1000 ? window.innerWidth / 3 : window.innerWidth / 4; + const detectableArea_right = window.innerWidth - detectableArea; + + let payload: {left?: boolean; right?: boolean} | undefined; + + if (direction.toLowerCase() === 'right') { + if (startPos < detectableArea) payload = {left: true}; + else if (startPos > detectableArea_right) payload = {right: false}; + } else if (direction.toLowerCase() === 'left') { + if (startPos < detectableArea) payload = {left: false}; + else if (startPos > detectableArea_right) payload = {right: true}; + } + return payload; +}; + +export const loadPomUrl = ( + color: string, + material: number +): Promise => { + return new Promise((resolve, reject) => { + const matName = material === 1 ? 'silk' : 'wool'; + const color_hex = color.replace('#', ''); + const hex_r = color_hex.substring(0, 2); + const hex_g = color_hex.substring(2, 4); + const colorPomImageUrl = `${AppProvider.domain}/permanentcache/colorpoms/${matName}/${hex_r}/${hex_g}/${color_hex}.png`; + readImage(colorPomImageUrl) + .then(() => resolve(colorPomImageUrl)) + .catch(reject); + }); +}; + +type ScrollDirection = 'left' | 'right' | 'up' | 'down'; + +export const handleNavigation = ({ + elem, + direction, +}: { + elem: HTMLElement | null; + direction: ScrollDirection; +}): void => { + if (!elem || !direction) return; + + const isHorizontal = direction === 'left' || direction === 'right'; + const isVertical = direction === 'up' || direction === 'down'; + + if (isHorizontal) { + const offsetValue = + direction === 'right' + ? elem.offsetWidth / 2 + : (-1 * elem.offsetWidth) / 2; + + if ( + Math.abs(elem.scrollLeft + elem.clientWidth - elem.scrollWidth) < 2 && + direction === 'right' + ) { + elem.scrollLeft = 0; + } else if (elem.scrollLeft === 0 && direction === 'left') { + elem.scrollLeft = elem.scrollWidth; + } else { + elem.scrollLeft += offsetValue; + } + } + + if (isVertical) { + const offsetValue = + direction === 'down' + ? elem.offsetHeight / 2 + : (-1 * elem.offsetHeight) / 2; + + if ( + Math.abs(elem.scrollTop + elem.clientHeight - elem.scrollHeight) < 2 && + direction === 'down' + ) { + elem.scrollTop = 0; + } else if (elem.scrollTop === 0 && direction === 'up') { + elem.scrollTop = elem.scrollHeight; + } else { + elem.scrollTop += offsetValue; + } + } +}; diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts new file mode 100644 index 0000000..a89a1d3 --- /dev/null +++ b/src/Utils/utils.ts @@ -0,0 +1,736 @@ +/* eslint-disable no-useless-escape */ +import axios from 'axios'; +import Pako from 'pako'; +export interface Size { + width: number; + height: number; +} + +interface SizedContainer { + offsetWidth: number; + offsetHeight: number; +} + +export interface OrderSheetDetails { + PhysicalHeight: number; + PhysicalWidth: number; + Unit: string; +} + +interface BoxSize { + Width: number; + Height: number; +} + +interface BooleanExpression { + variable?: string; + condition?: string; + operands?: BooleanExpression[]; +} + +interface ColorItem { + ColorName: string; + Color: string; +} + +interface PaletteItem { + Name: string; + Value: string; +} + +interface Vector3Like { + x: number; + y: number; + project(camera: unknown): Vector3Like; +} + +declare global { + interface Window { + flags: { + ordersheet?: { + showAreaInM2?: boolean; + showTotalAreaInYards?: boolean; + }; + visualizations?: { + wallToWallCenterRepeat?: {x?: boolean; y?: boolean}; + }; + inhouseChanges?: { + saveFunctionForFlutter?: boolean; + }; + }; + TextureOptions: { + DefaultTextureNames: string[]; + AdditionalTextureNames?: string[]; + CustomTextureIndices: number[]; + CustomTextureNames: string[]; + }; + } +} + +export const convertUnits = ( + value: number, + from: string, + to: string +): number => { + let convertedValue = value; + if (from === 'ft') { + if (to === 'in') convertedValue = value * 12; + else if (to === 'cm') convertedValue = value * 30.47999902464003; + } else if (from === 'in') { + if (to === 'ft') convertedValue = value / 12; + else if (to === 'cm') convertedValue = value * 2.5399999187200026; + } else if (from === 'cm') { + if (to === 'ft') convertedValue = value * 0.0328084; + else if (to === 'in') convertedValue = value * 0.3937008; + } + return convertedValue; +}; + +export function convertArrIntoRad(arrDeg: number[]): number[] { + return arrDeg.map((angle) => (angle * Math.PI) / 180); +} + +export const convertArrintoDeg = (arrRad: number[]): number[] => + arrRad.map((angle) => (angle * 180) / Math.PI); + +export const degToRad = (deg: number): number => (deg * Math.PI) / 180; +export const radToDeg = (deg: number): number => (deg * 180) / Math.PI; + +export function fitImageToContainer(image: Size, container: Size): Size { + let {width: containerwidth, height: containerheight} = container; + let {width: imagewidth, height: imageheight} = image; + let width = imagewidth, + height = imageheight; + if (imagewidth > imageheight) { + if (width > containerwidth) { + height = (imageheight * containerwidth) / imagewidth; + width = containerwidth; + } + if (height > containerheight) { + width = (imagewidth * containerheight) / imageheight; + height = containerheight; + } + } else { + if (height > containerheight) { + width = (imagewidth * containerheight) / imageheight; + height = containerheight; + } + if (width > containerwidth) { + height = (imageheight * containerwidth) / imagewidth; + width = containerwidth; + } + } + return {width, height}; +} + +export const readJSON = (url: string): Promise => + axios.get(url).then((response) => response.data as unknown); + +export const shuffleArray = (array: T[]): T[] => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +}; + +export const constructBooleanExpression = (data: unknown): string => { + if (!data || typeof data !== 'object') return ''; + const operatorsMap: Record = { + OR: '||', + AND: '&&', + LT: '<', + GT: '>', + EQ: '==', + NEQ: '!=', + GTE: '>=', + LTE: '<=', + }; + + function process(node: BooleanExpression): string { + if (node.variable) return node.variable; + if (node.operands) { + const operator = operatorsMap[node.condition ?? ''] ?? ''; + return `(${node.operands.map(process).join(` ${operator} `)})`; + } + return ''; + } + return process(data as BooleanExpression); +}; + +export const findSingleTrueCondition = ( + conditions: BooleanExpression[], + variables: Record +): number | null => { + let trueIndex = -1; + + for (let i = 0; i < conditions.length; i++) { + const expression = constructBooleanExpression( + conditions[i].condition ? conditions[i] : conditions[i].operands?.[0] + ); + const filledExpression = expression.replace(/\b\w+\b/g, (match) => + variables[match] !== undefined ? String(variables[match]) : match + ); + // eslint-disable-next-line no-eval + const isTrue = eval(filledExpression); + if (isTrue) return i; + } + + if (trueIndex === -1) return null; + + return trueIndex; +}; + +export function fitIntoContainer(image: Size, container: SizedContainer): Size { + const {offsetWidth: containerwidth, offsetHeight: containerheight} = + container; + const imagewidth = image.width, + imageheight = image.height; + if (!image.width || !image.height) + return {width: containerwidth, height: containerheight}; + let width = imagewidth, + height = imageheight; + const wdif = width - containerwidth; + const hdif = height - containerheight; + if (imagewidth > imageheight) { + if (wdif > hdif) { + height = containerheight; + width = (imagewidth * containerheight) / imageheight; + } else { + width = containerwidth; + height = (imageheight * containerwidth) / imagewidth; + } + } + return {width, height}; +} + +export function resizeKeepingAspect( + image: Size, + container: Size, + fitType = 'fit_inside', + resolution = 1 +): {width: number; height: number; xOffset: number; yOffset: number} { + let {width: containerwidth, height: containerheight} = container; + let {width: imagewidth, height: imageheight} = image; + if (!imagewidth || !imageheight) + return { + width: containerwidth, + height: containerheight, + xOffset: 0, + yOffset: 0, + }; + if (containerheight === 0 || containerwidth === 0) + return {width: imagewidth, height: imageheight, xOffset: 0, yOffset: 0}; + let width = imagewidth, + height = imageheight; + let xOffset = 0; + let yOffset = 0; + + switch (fitType) { + case 'fit_inside': + if (imagewidth > imageheight) { + if (width > containerwidth) { + height = (imageheight * containerwidth) / imagewidth; + width = containerwidth; + } + if (height > containerheight) { + width = (imagewidth * containerheight) / imageheight; + height = containerheight; + } + } else { + if (height > containerheight) { + width = (imagewidth * containerheight) / imageheight; + height = containerheight; + } + if (width > containerwidth) { + height = (imageheight * containerwidth) / imagewidth; + width = containerwidth; + } + } + break; + case 'crop': { + const wdif = width - containerwidth; + const hdif = height - containerheight; + if (wdif > hdif) { + height = containerheight; + width = (imagewidth * containerheight) / imageheight; + } else { + width = containerwidth; + height = (imageheight * containerwidth) / imagewidth; + } + xOffset = (containerwidth - width) / 2; + yOffset = (containerheight - height) / 2; + break; + } + default: + break; + } + width = width * resolution; + height = height * resolution; + return {width, height, xOffset, yOffset}; +} + +export const convertUnit = ( + from: string, + to: string, + value: number, + fixed?: number, + customConversion = false, + customInToCmFactor = 2.5 +): number => { + let converted = convertUnits(value, from, to); + if (customConversion) { + if (from === 'in' && to === 'cm') converted = value * customInToCmFactor; + else if (from === 'cm' && to === 'in') + converted = value / customInToCmFactor; + else if (from === 'ft' && to === 'cm') + converted = value * customInToCmFactor * 12; + else if (from === 'cm' && to === 'ft') + converted = value / (customInToCmFactor * 12); + } + if (fixed) return Number(converted.toFixed(fixed)); + return converted; +}; + +export const convertUnit_Arr = ( + from: string, + to: string, + valueArr: number[], + setCustomConversion = false, + customInToCmFactor?: number +): number[] => + valueArr.map((val) => + convertUnit(from, to, val, 2, setCustomConversion, customInToCmFactor) + ); + +export const getExtension = (path: string): string => { + const fp = path.split('.'); + return fp[fp.length - 1]; +}; + +export function isiPhone(): boolean { + return ( + navigator.platform.indexOf('iPhone') !== -1 || + navigator.platform.indexOf('iPad') !== -1 || + navigator.platform.indexOf('iPod') !== -1 + ); +} + +export function convertNumberToFeetInch( + f: number, + unit: string +): number | string { + if (unit !== 'ft') return f; + let ft = Math.floor(f); + let inch = Math.round(12 * (f - ft)); + if (inch === 12) { + ft++; + inch = 0; + } + return ft + '′' + (inch > 0 ? inch + '″' : ''); +} + +export function convertFeetInchToNumber( + f: string | number, + unit: string +): number | null { + if (unit !== 'ft') return typeof f === 'number' ? f : parseFloat(f) || null; + const rex = /[-+]?[0-9]*\.?[0-9]+/g; + const match = String(f).match(rex); + if (match) { + const feet = parseFloat(match[0]); + const inch = match.length > 1 ? parseFloat(match[1]) : 0; + if (feet > 0 && inch >= 0 && inch < 12) return feet + inch / 12; + } + return null; +} + +export const displayAreaOfRug = ( + orderSheetDetails: OrderSheetDetails +): number => { + let displayArea = + orderSheetDetails.PhysicalHeight * orderSheetDetails.PhysicalWidth; + if (orderSheetDetails.Unit === 'cm') displayArea = displayArea / 10000; + return displayArea; +}; + +export const displayAreaInFt = ( + orderSheetDetails: OrderSheetDetails, + setCustomConversion?: boolean, + customInToCmFactor?: number +): number => { + let displayArea = displayAreaOfRug(orderSheetDetails); + if (orderSheetDetails.Unit === 'cm') { + const [PhysicalWidth, PhysicalHeight] = convertUnit_Arr( + orderSheetDetails.Unit, + 'ft', + [orderSheetDetails.PhysicalWidth, orderSheetDetails.PhysicalHeight], + setCustomConversion, + customInToCmFactor + ); + displayArea = PhysicalWidth * PhysicalHeight; + } + return displayArea; +}; + +export const displayUnitOfRug = (unit: string): string => + unit === 'cm' ? 'm' : unit; + +export const calculateTotalArea = ( + orderSheetDetails: OrderSheetDetails +): string => { + const unit = orderSheetDetails.Unit; + let totalArea = `${ + Math.round(displayAreaOfRug(orderSheetDetails) * 100) / 100 + } sq. ${displayUnitOfRug(unit)}.`; + if (window.flags.ordersheet?.showAreaInM2) { + totalArea = `${ + Math.round(displayAreaOfRug(orderSheetDetails) * 100) / 100 + } ${displayUnitOfRug( + unit + )}²`; + } + if (window.flags.ordersheet?.showTotalAreaInYards) + totalArea = `${ + Math.round((displayAreaOfRug(orderSheetDetails) / 9) * 100) / 100 + } sq. Yds. `; + return totalArea; +}; + +export function makeid(length: number): string { + let result = ''; + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +export const leftFillNum = (num: number, targetLength: number): string => + num.toString().padStart(targetLength, '0'); + +export const convertTilePointToName = (i: number, j: number): string => + `${leftFillNum(i, 2)}_${leftFillNum(j, 2)}`; + +export const convertNameToTilePoint = ( + name: string +): {x: number; y: number; name: string} => { + const x = parseInt(name.trim().substring(0, 2)); + const y = parseInt(name.trim().substring(3, 5)); + return {x, y, name}; +}; + +export const getPathFromString = (string: string): string => { + const x = string.split('/'); + x.pop(); + return x.join('/'); +}; + +export const createAsyncQueue = ( + tasks: Promise[], + maxNumOfWorkers = 5 +): Promise => { + let numOfWorkers = 0; + let taskIndex = 0; + + return new Promise((done) => { + const handleResult = (index: number) => (result: unknown) => { + tasks[index] = Promise.resolve(result); + numOfWorkers--; + getNextTask(); + }; + const getNextTask = () => { + if (numOfWorkers < maxNumOfWorkers && taskIndex < tasks.length) { + tasks[taskIndex] + .then(handleResult(taskIndex)) + .catch(handleResult(taskIndex)); + taskIndex++; + numOfWorkers++; + getNextTask(); + } else if (numOfWorkers === 0 && taskIndex === tasks.length) { + done(tasks); + } + }; + getNextTask(); + }); +}; + +export const areaOfellipse = (x: number, y: number): number => Math.PI * x * y; + +export const createVector = ( + p: Vector3Like, + camera: unknown, + width: number, + height: number +): Vector3Like => { + const vector = p.project(camera); + vector.x = ((vector.x + 1) / 2) * width; + vector.y = (-(vector.y - 1) / 2) * height; + return vector; +}; + +export const getDesignPathInTitle = (designPath: string): string => { + if (!designPath) return ''; + const dotPos = designPath.lastIndexOf('.'); + const trimmed = designPath.substring(0, dotPos); + return trimmed.replace(/\/\./g, '/').split('/').slice(1).join('/'); +}; + +export const isIos: boolean = + !!navigator.userAgent.match(/Safari/i) && + !navigator.userAgent.match(/Chrome/i); +export const isAppleDevice: boolean = navigator.userAgent.includes('Mac'); +export const isTouchScreen: boolean = navigator.maxTouchPoints >= 1; +export const isMobileDevice: boolean = + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iPhone|iPod|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)vodafone|wap|windows (ce|phone)|xda|xiino/i.test( + navigator.userAgent + ) || + (isAppleDevice && isTouchScreen) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + navigator.userAgent.substr(0, 4) + ); + +export const shuffle = (arra1: T[]): T[] => { + let ctr = arra1.length; + let temp: T; + let index: number; + while (ctr > 0) { + index = Math.floor(Math.random() * ctr); + ctr--; + temp = arra1[ctr]; + arra1[ctr] = arra1[index]; + arra1[index] = temp; + } + return arra1; +}; + +export const shuffleForLocked = ( + arra1: T[], + temparr: number[] = [] +): T[] => { + let temp: T; + let index: number; + + const unlockedArray: T[] = []; + arra1.forEach((item, i) => { + if (!temparr.includes(i)) unlockedArray.push(item); + }); + let ctr = unlockedArray.length; + + if (ctr === 2) { + const temp1 = unlockedArray[0]; + unlockedArray[0] = unlockedArray[1]; + unlockedArray[1] = temp1; + } else { + while (ctr > 0) { + index = Math.floor(Math.random() * ctr); + ctr--; + temp = unlockedArray[ctr]; + unlockedArray[ctr] = unlockedArray[index]; + unlockedArray[index] = temp; + } + } + + let unctr = 0; + arra1.forEach((_item, i) => { + if (!temparr.includes(i)) { + arra1.splice(i, 1, unlockedArray[unctr]); + unctr++; + } + }); + return arra1; +}; + +export const getPalette = (arra1: ColorItem[]): PaletteItem[] => { + let ctr = arra1.length; + const newColorPalette: PaletteItem[] = new Array(ctr); + while (ctr > 0) { + ctr--; + newColorPalette[ctr] = { + Name: arra1[ctr].ColorName, + Value: arra1[ctr].Color, + }; + } + return newColorPalette; +}; + +export const safelyParseJSON = (json: string): unknown => { + let parsed: unknown; + try { + parsed = JSON.parse(json); + } catch { + // intentionally empty + } + return parsed; +}; + +export const decodeColorsFromString = (str: string): unknown => { + try { + const binaryStr = window.atob(str); + const bytes = Uint8Array.from(binaryStr, (c) => c.charCodeAt(0)); + return safelyParseJSON(Pako.inflate(bytes, {to: 'string'})); + } catch (exception) { + console.error(exception); + return null; + } +}; + +export const encodeColorsToString = (colors: unknown): string => { + const compressed = Pako.deflate(JSON.stringify(colors)); + const binaryStr = Array.from(compressed, (c) => String.fromCharCode(c)).join(''); + return window.btoa(binaryStr); +}; + +export function getMaterialNameArray(): string[] { + const materialArr: string[] = new Array(70).fill(''); + window.TextureOptions.DefaultTextureNames.forEach((name, i) => { + materialArr[i] = name; + }); + if (window.TextureOptions.AdditionalTextureNames) { + window.TextureOptions.AdditionalTextureNames.forEach((name, i) => { + materialArr[i + 4] = name; + }); + } + window.TextureOptions.CustomTextureIndices.forEach((texIndex, i) => { + materialArr[texIndex] = window.TextureOptions.CustomTextureNames[i]; + }); + return materialArr; +} + +export const errorTypes = { + INTERFACE_ERROR: {message: 'INTERFACE_ERROR'}, + ANICENT_BROWSER: {message: 'ANICENT_BROWSER'}, + NO_CONNECTION: {message: 'NO_CONNECTION'}, + INVALID_LINK: {message: 'INVALID_LINK'}, + SESSION_TIMEOUT: {message: 'SESSION_TIMEOUT'}, + URL_INCOMPLETE: {message: 'URL_INCOMPLETE'}, +}; + +export const toastErrors = { + NO_CONNECTION: { + message: 'Looks like you got disconnected. Check your internet connection.', + }, + SERVER_ERROR: { + message: 'We’re having some issues. Please try again in a while.', + }, +}; + +export const doesStringMatch = (string: string, searchKey: string): boolean => + string.toLowerCase().indexOf(searchKey.toLowerCase()) !== -1; + +export const replaceAt = ( + string: string, + index: number, + replace: string +): string => string.substring(0, index) + replace + string.substring(index + 1); + +export const replaceAll = ( + string: string, + replace: string, + replaceWith: string +): string => string.replace(new RegExp(replace, 'g'), replaceWith); + +export function makeUrl( + ...args: (string | number | null | undefined | false)[] +): string { + let res = ''; + args.forEach((argument, index) => { + if (argument) { + res = `${res}${index === 0 ? '' : '/'}${argument}`; + } + }); + return res; +} + +export function getCroppedSize( + srcSize: BoxSize, + dstSize: BoxSize, + cropPadding = 0 +): {width: number; height: number; offsetX: number; offsetY: number} { + let width = dstSize.Width, + height = dstSize.Height; + const {Width: canvasWidth, Height: canvasHeight} = srcSize; + const wr = dstSize.Width / canvasWidth; + const hr = dstSize.Height / canvasHeight; + if (wr > hr) { + if (dstSize.Width > canvasWidth) { + width = canvasWidth; + height = (dstSize.Height * width) / dstSize.Width; + } + } else { + if (dstSize.Height > canvasHeight) { + height = canvasHeight; + width = (dstSize.Width * height) / dstSize.Height; + } + } + const shouldApplyPad = + (width / height).toFixed(1) !== (canvasWidth / canvasHeight).toFixed(1); + if (shouldApplyPad) { + width = width - cropPadding; + height = height - cropPadding; + } + width = Math.ceil(width); + height = Math.ceil(height); + const offsetX = (width - srcSize.Width) / 2; + const offsetY = (height - srcSize.Height) / 2; + return {width, height, offsetX, offsetY}; +} + +export const getDesignDimensionsString = ({ + designDetails, + showOneDimensionIfSquare = false, + showDesignDimensions = true as boolean | number, + showDimensionInInches = false, +}: { + designDetails: OrderSheetDetails; + showOneDimensionIfSquare?: boolean; + showDesignDimensions?: boolean | number; + showDimensionInInches?: boolean; +}): string => { + const {PhysicalHeight, PhysicalWidth, Unit} = designDetails; + const widthString = convertNumberToFeetInch(PhysicalWidth, Unit); + const heightString = ` x ${convertNumberToFeetInch(PhysicalHeight, Unit)}`; + const combinedStr = `${widthString}${ + showOneDimensionIfSquare && PhysicalHeight === PhysicalWidth + ? '' + : heightString + }`; + let designDimensionsString = `${combinedStr} ${Unit === 'ft' ? '' : Unit}`; + + if (showDesignDimensions === 2 && Unit) { + const unit = Unit === 'ft' ? 'cm' : 'ft'; + const wid = convertUnit(Unit, unit, PhysicalWidth, 2) || PhysicalWidth; + const hgt = convertUnit(Unit, unit, PhysicalHeight, 2) || PhysicalHeight; + const widthStr = convertNumberToFeetInch(wid, unit); + const heightStr = ` x ${convertNumberToFeetInch(hgt, unit)}`; + const string_unit = `${widthStr}${ + showOneDimensionIfSquare && PhysicalHeight === PhysicalWidth + ? '' + : heightStr + } ${unit === 'ft' ? '' : unit}`; + designDimensionsString = `${designDimensionsString} (${string_unit})`; + } + if (showDimensionInInches && PhysicalWidth && PhysicalHeight) { + const wid = convertUnit(Unit, 'in', PhysicalWidth, 2); + const hgt = convertUnit(Unit, 'in', PhysicalHeight, 2); + designDimensionsString = `${wid}″ x ${hgt}″`; + } + return designDimensionsString; +}; + +export const createUriSafe = (uriString: string): string => + uriString.split('/').map(encodeURIComponent).join('/'); + +export const calculateKLratio = ({ + Height, + PhysicalHeight, + Width, + PhysicalWidth, +}: { + Height: number; + PhysicalHeight: number; + Width: number; + PhysicalWidth: number; +}): number => (Width * PhysicalHeight) / (PhysicalWidth * Height); diff --git a/src/react-wrapper/roomView-react.ts b/src/react-wrapper/roomView-react.ts new file mode 100644 index 0000000..dd15aee --- /dev/null +++ b/src/react-wrapper/roomView-react.ts @@ -0,0 +1,15 @@ +// react-wrapper/index.ts +import {createComponent} from '@lit/react'; +import React from 'react'; +import {RoomIllustration} from '../Components/RoomView'; + +export const RoomIllustrationReact = createComponent({ + react: React, + tagName: 'room-illustration', + elementClass: RoomIllustration, + events: { + onRenderingComplete: 'rendering-complete', + onSizesChange: 'sizes-change', + onActiveClick: 'active-click', + }, +}); diff --git a/tsconfig.json b/tsconfig.json index 9a47b63..694987d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "noFallthroughCasesInSwitch": true, "noImplicitAny": true, "noImplicitThis": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, @@ -29,6 +29,6 @@ ], "types": ["mocha"] }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/Components/RoomView/helpers/RoomViewHelper.js"], "exclude": [] } diff --git a/yarn.lock b/yarn.lock index 7077243..1e6c54c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -913,6 +913,11 @@ dependencies: es-module-lexer "^0.9.3" +"@dimforge/rapier3d-compat@~0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz#7b3365e1dfdc5cd957b45afe920b4ac06c7cd389" + integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow== + "@esbuild/aix-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" @@ -1592,6 +1597,11 @@ resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz" integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== +"@tweenjs/tween.js@~23.1.3": + version "23.1.3" + resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz#eff0245735c04a928bb19c026b58c2a56460539d" + integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA== + "@types/accepts@*": version "1.3.7" resolved "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz" @@ -1787,6 +1797,11 @@ dependencies: undici-types "~7.19.0" +"@types/pako@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/pako/-/pako-2.0.4.tgz#c3575ef8125e176c345fa0e7b301c1db41170c15" + integrity sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw== + "@types/parse5@^6.0.1": version "6.0.3" resolved "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz" @@ -1867,6 +1882,23 @@ resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz" integrity sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w== +"@types/stats.js@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.4.tgz#1933e5ff153a23c7664487833198d685c22e791e" + integrity sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA== + +"@types/three@^0.184.0": + version "0.184.0" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.184.0.tgz#f6a3ea283bebb65e5de6ff4dbf18ef57bdcf5bcc" + integrity sha512-4mY2tZAu0y0B0567w7013BBXSpsP0+Z48NJvmNo4Y/Pf76yCyz6Jw4P3tUVs10WuYNXXZ+wmHyGWpCek3amJxA== + dependencies: + "@dimforge/rapier3d-compat" "~0.12.0" + "@tweenjs/tween.js" "~23.1.3" + "@types/stats.js" "*" + "@types/webxr" ">=0.5.17" + fflate "~0.8.2" + meshoptimizer "~1.1.1" + "@types/trusted-types@^2.0.2": version "2.0.7" resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" @@ -1877,6 +1909,11 @@ resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.15.10.tgz#742b77ec34d58554b94a76a14cef30d59e3c16b9" integrity sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA== +"@types/webxr@>=0.5.17": + version "0.5.24" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.24.tgz#734d5d90dadc5809a53e422726c60337fa2f4a44" + integrity sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg== + "@types/ws@^7.4.0": version "7.4.7" resolved "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz" @@ -2624,6 +2661,11 @@ async@^3.2.6: resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + atob@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" @@ -2648,6 +2690,15 @@ axios@0.21.4: dependencies: follow-redirects "^1.14.0" +axios@^1.15.2: + version "1.15.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.2.tgz#eb8fb6d30349abace6ade5b4cb4d9e8a0dc23e5b" + integrity sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A== + dependencies: + follow-redirects "^1.15.11" + form-data "^4.0.5" + proxy-from-env "^2.1.0" + babel-plugin-polyfill-corejs2@^0.4.15: version "0.4.17" resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz" @@ -3201,6 +3252,13 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + command-line-args@5.1.2, command-line-args@^5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.2.tgz" @@ -3540,6 +3598,11 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + delegates@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" @@ -3828,6 +3891,16 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: dependencies: es-errors "^1.3.0" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + esbuild@^0.21.3: version "0.21.5" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz" @@ -4143,6 +4216,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fflate@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" @@ -4229,7 +4307,7 @@ flatted@^3.2.9: resolved "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz" integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== -follow-redirects@^1.0.0, follow-redirects@^1.14.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.15.11: version "1.16.0" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz" integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== @@ -4254,6 +4332,17 @@ foreground-child@^3.1.0: cross-spawn "^7.0.6" signal-exit "^4.0.1" +form-data@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz" @@ -4324,7 +4413,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -5620,6 +5709,11 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +meshoptimizer@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-1.1.1.tgz#20c7d050d59bdc573154814f6a37d47d89908cca" + integrity sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g== + micromatch@^3.1.10: version "3.1.10" resolved "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz" @@ -5652,7 +5746,7 @@ mime-db@1.52.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -6063,6 +6157,11 @@ package-json-from-dist@^1.0.0: resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== +pako@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -6275,6 +6374,11 @@ proxy-from-env@1.1.0: resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +proxy-from-env@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" + integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA== + prr@~1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz" @@ -6413,6 +6517,11 @@ puppeteer-core@^19.8.1: unbzip2-stream "1.4.3" ws "8.13.0" +pure-rand@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-8.4.0.tgz#1d9e26e9c0555486e08ae300d02796af8dec1cd0" + integrity sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A== + qs@^6.5.2: version "6.15.1" resolved "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz" @@ -7292,6 +7401,11 @@ text-table@^0.2.0: resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +three@^0.184.0: + version "0.184.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.184.0.tgz#5bca0a3851eea5345e4c205567b40dfa49b791b5" + integrity sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg== + throttleit@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4"