Source: index.js

import * as THREE from 'three'

import { loadSheet } from './Sheet.js';

import { AmbientLight, CanvasTexture, Color, DoubleSide, MeshStandardMaterial, PerspectiveCamera, PointLight, Shape, Vector3, WebGLRenderer } from 'three';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { VertexNormalsHelper } from 'three/examples/jsm/helpers/VertexNormalsHelper.js';
import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils";
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { HalftonePass } from 'three/examples/jsm/postprocessing/HalftonePass.js';
import { LuminosityShader } from 'three/examples/jsm/shaders/LuminosityShader.js';
import { SobelOperatorShader } from 'three/examples/jsm/shaders/SobelOperatorShader.js';
import { RenderPixelatedPass } from 'three/examples/jsm/postprocessing/RenderPixelatedPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js';

import { GroundProjectedEnv } from './GroundProjectedEnv.js';
import { getDynamicEnv } from './DynamicEnvMap.js';
import { exporter } from './Exporters.js'
import { ModelViewerScene } from './ModelViewerScene.js';
import { getGeometry, setFont3D, setQuality } from './Geometry.js';
import { GPUPicker } from './GPUPicker.js';
import { Group3D } from './Group3D.js';
import { Mesh3D } from './Mesh3D.js';
import { Inspector } from './Inspector.js';
import { booleanOperation } from './BooleanOperations.js';
import { getUIButton, insertStyleSheet } from './UI.js';
import { loadAsset, loadObject } from './Importers.js';
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils.js';
import { AnaglyphEffect } from 'three/examples/jsm/effects/AnaglyphEffect.js';
import { FirstPersonControls } from 'three/examples/jsm/controls/FirstPersonControls.js';

import { notify, REASON_CANNOT, REASON_ERROR, REASON_CHECK } from './SmartNotifications.js';
var renderer, cam, scene, picker, orbit;
var composer;
var anaglyphEffect;


var defaultEnvironment;
var toolboxContainer;

const FULL_HD = 'Full HD';
const HD = 'HD';
const SD = 'SD';
const LD = 'LD';
const FORWARD = 'forward';
const BACKWARD = 'backward';

p5.prototype.GroundProjectedEnv = GroundProjectedEnv;
p5.prototype.getDynamicEnv = getDynamicEnv;
//Environments constants

const CINEMA = "https://dev.codemotionkids.com/libraries/diorama/environments/cinema/";
const SWITZERLAND = "https://dev.codemotionkids.com/libraries/diorama/environments/switzerland/";
const ROTUNDA = "https://dev.codemotionkids.com/libraries/diorama/environments/rotunda/";
const SUNSET = "https://dev.codemotionkids.com/libraries/diorama/environments/sunset/";
const NIGHT = "https://dev.codemotionkids.com/libraries/diorama/environments/night/";
const NEON = "https://dev.codemotionkids.com/libraries/diorama/environments/neon/";
const VERANDA = "https://dev.codemotionkids.com/libraries/diorama/environments/veranda/";
const STARS = "https://dev.codemotionkids.com/libraries/diorama/environments/stars/";
const FOREST = "https://dev.codemotionkids.com/libraries/diorama/environments/forest/";
const UBUNTU = "https://dev.codemotionkids.com/libraries/diorama/fonts/Ubuntu_Regular.json";

const presets = [SUNSET, CINEMA, ROTUNDA, SWITZERLAND, NIGHT, NEON, VERANDA, STARS, FOREST];

const GLITCH = 0;
const LUMINOSITY = 1;
const SOBEL = 2;
const BLOOM = 3;
const PIXEL = 4;
const HALFTONE = 5;
const DEPTH = 6;


const HALFTONE_DOT = 1;
const HALFTONE_ELLIPSE = 2;
const HALFTONE_LINE = 3;
const HALFTONE_SQUARE = 4;


const ORTHOGRAPHIC = 0;
const PERSPECTIVE = 1;
const CAMERA_DEFAULT_POSITION_PERSPECTIVE = new Vector3(0, 1.68, 5);
const CAMERA_DEFAULT_POSITION_ORTHOGRAPHIC = new Vector3(8, 8, 8);

const groupStack = [];

const FLAT = true;
const SMOOTH = false;
var currentShading = SMOOTH;
var inspector;
var canvasCreated = false;
var currentScene;
var currentNormalMap;
var currentDiffuseMap;
var currentRoughnessMap;
var currentAOMap;
var currentAOMapIntensity = 1.0;
var currentMetalnessMap;
var currentDisplacementMap;
var currentDisplacementScale = 1.0;
var currentBumpMap;
var currentBumpScale = 1.0;
var currentEnvMap;
var currentMaterial = new MeshStandardMaterial();
var currentColor = new Color();

var currentExtrude = null;
var currentLathe = null;
var currentLatheSides = 32;
var currentExtrudeShape;
var currentSubdivisions = {
    subdivisions: 0,
    split: true,       // optional, default: true
    uvSmooth: false,      // optional, default: false
    preserveEdges: false,      // optional, default: false
    flatOnly: false,      // optional, default: false
    maxTriangles: 3000,   // optional, default: 3000
};;

const NONE = -1;
const EXTRUDING = 0;
const SHAPING = 1;
const LATHING = 2;
var vertexContext = NONE;

var currentShape = null;
var currentAlign = ['center'];

var last = null;

var fontLoader;
/**
 * 
 * Load a font in typeface.js format. You can convert a font file to JSON using https://gero3.github.io/facetype.js/
 * You can also use constants like <pre>UBUNTU</pre>
 * 
 * @param {String} url typeface.js json file
 * @param {Function} callback Optional function called when file is loaded
 * @returns {Font} loaded font
 */

const loadFont3D = (url, callback) => {
    if (!fontLoader) {
        fontLoader = new FontLoader();
    }
    const result = {};

    fontLoader.load(
        // resource URL
        url,

        // onLoad callback
        function (font) {
            // do something with the font
            result.font = font;
            self._decrementPreload();
            if (typeof callback == 'function') {
                callback(result); // do the callback.
            }
        },

        // onProgress callback
        null,

        // onError callback
        function (err) {
            notify('An error happened loading font from ' + url, REASON_ERROR);
        });


    return result;


};
/**
 * 
 * Add a 3D text to your scene.
 * Remember to {@link loadFont3D}
 * 
 * @param {Number} x 
 * @param {Number} y 
 * @param {Number} z 
 * @param {String} text 
 * @returns Text in 3D
 */
const text3D = (x = 0, y = 0, z = 0, text = "?!?") => {

    const geometry = getGeometry({
        type: 'text',
        text: text
    })
    if (geometry)
        return addMesh(x, y, z, geometry);
}

/**
 * Transforms a 3D position into a 2D position on the screen.
 *
 * @param {...(number|THREE.Vector3)} args - If one argument is provided, it should be a Vector3 representing the position in 3D space. If three arguments are provided, they should be x, y, and z coordinates in 3D space.
 * @returns {THREE.Vector3|null} - Returns a Vector3 with x and y coordinates representing the position on the screen, or null if the input arguments are not valid.
 */

const onScreen = (...args) => {
    var position;
    if (args.length == 1) {
        position = args[0];
    } else if (args.length == 3) {
        position = new THREE.Vector3(args[0], args[1], args[2]);
    } else {
        console.warn("You must use onScreen() with x, y, z or a Vector3");
        return null;
    }
    var onScreen = position.clone();
    onScreen.project(getCamera());
    onScreen.x = floor(((onScreen.x + 1) * width) * 0.5);
    onScreen.y = floor((-(onScreen.y - 1) * height) * 0.5);
    onScreen.z = 0;

    return onScreen;
}


const getPresets = () => {
    return presets
}

/**
 * Create a 3D canvas containing your 3D scene
 * @param {Integer} width - How large
 * @param {Integer} height - How tall
 */
const createCanvas3D = (width, height, disableControls = false) => {
    //noCanvas();
    canvasCreated = true;

    document.body.appendChild(renderer.domElement);
    inspector = new Inspector();

    if (width && height) {
        p5.instance.createCanvas(width, height);
    }

    if (p5.instance.canvas) {

        renderer.setSize(p5.instance.width, p5.instance.height);
        p5.instance.canvas.style.position = "absolute";
        const boundingRect = p5.instance.canvas.getBoundingClientRect();
        renderer.domElement.style = `position: absolute; top: ${boundingRect.y + boundingRect.top}; left: ${boundingRect.x + boundingRect.left}; z-index: ${p5.instance.canvas.style.zIndex - 1}`;
        cam.aspect = p5.instance.width / p5.instance.height;
        cam.updateProjectionMatrix();

        // p5.instance.canvas.addEventListener('resize', () => {
        //     console.log(p5.instance.width, p5.instance.height)
        //     renderer.setSize(p5.instance.width, p5.instance.height);
        //     cam.aspect = p5.instance.width / p5.instance.height;
        //     cam.updateProjectionMatrix();
        // })
    }


    // p5.instance._setProperty("width", width);
    // p5.instance._setProperty("height", height);
    // p5.instance._setProperty("canvas", renderer.domElement);

    orbit = new OrbitControls(cam, p5.instance.canvas);
    if (disableControls)
        orbit.enabled = false;
    p5.instance.angleMode(p5.prototype.DEGREES);
    p5.instance.quality(HD);

    const downloadButton = getUIButton("๐ŸŸก Download", "#ff0040", () => {
        exportGLTF(scene)
    })
    // const downloadButtonPrint = getUIButton("STL", "#4caf50", () => {
    //     exportSTL(scene)
    // })
    const downloadButtonPNG = getUIButton("๐Ÿ–ผ๏ธ Screenshot", "#ff5722", () => {
        exportPNG()
    })

    const helpButton = getUIButton("๐Ÿ“‘ Help", "#2196f3", () => {
        window.open('https://dev.codemotionkids.com/libraries/diorama/docs/', '_blank');
    });

    const exportButton = getUIButton("โœจ Export", "#3f51b5", () => {
        exportObject(scene)
    })
    toolboxContainer = document.createElement('div');
    toolboxContainer.classList.add('diorama-toolbox');
    toolboxContainer.appendChild(downloadButton);
    toolboxContainer.appendChild(downloadButtonPNG);
    toolboxContainer.appendChild(helpButton);
    toolboxContainer.appendChild(exportButton);

    renderer.domElement.after(toolboxContainer);
    const boundingRectRenderer = renderer.domElement.getBoundingClientRect();
    toolboxContainer.style.top = boundingRectRenderer.height + "px";
    return renderer.domElement;
};

const resizeCanvas3D = (w, h) => {

    p5.instance.resizeCanvas(w, h);
    renderer.setSize(p5.instance.width, p5.instance.height);
    const boundingRectRenderer = p5.instance.canvas.getBoundingClientRect();
    renderer.domElement.style = `position: absolute; top: ${boundingRectRenderer.y + boundingRectRenderer.top}; left: ${boundingRectRenderer.x + boundingRectRenderer.left}; z-index: ${p5.instance.canvas.style.zIndex - 1}`;
    cam.aspect = p5.instance.width / p5.instance.height;
    cam.updateProjectionMatrix();
    toolboxContainer.style.top = boundingRectRenderer.height + "px";
}

const init3D = () => {


    renderer = new WebGLRenderer({
        antialias: true,
        alpha: true,
        powerPreference: 'low-power'
    });

    renderer.toneMapping = THREE.LinearToneMapping;
    renderer.needsUpdate = true;
    scene = new ModelViewerScene(renderer);
    defaultEnvironment = scene.environment;

    cam = new PerspectiveCamera(
        75,
        1, //aspect
        0.1,
        1000
    );
    //renderer.addRenderPass(scene, cam);
    cam.position.z = 5;
    cam.position.y = 1.68;

    currentScene = scene;

    picker = new GPUPicker(renderer, scene, cam);

    if (exporter) {
        exporter(renderer, cam);
    }

    insertStyleSheet();
};

const render3D = function () {
    if (!canvasCreated)
        return;

    if (renderer.stats)
        renderer.stats.update();
    //    info = renderer.info.render.triagles;

    if (orbit && orbit.enabled)
        orbit.update();


    if (anaglyphEffect) {
        anaglyphEffect.render(scene, cam);
    } else if (composer && composer.passes.length > 1)
        composer.render();
    else
        renderer.render(scene, cam);
};

/**
 * Enables or disables the anaglyph effect, which creates a 3D stereoscopic image from two 2D images.
 * 
 * To enable this effect simply call anaglyph() function. anaglyph(false) will disable it.
 *
 * @param {boolean} enabled - A flag indicating whether the anaglyph effect should be enabled or disabled.
 * @returns {boolean} Current status of the anaglyph effect.
 */

const anaglyph = (enabled = true) => {

    if (enabled) {
        notify("You need 3D glasses (with red and cyan lenses) to see this!", "๐Ÿ”ด-๐Ÿ”ต")
        anaglyphEffect = new AnaglyphEffect(renderer, width, height);
    } else {
        anaglyphEffect = null;
    }
    return enabled
}

/**
 * Remove all special effects.
 */

const clearFX = () => {
    while (composer.passes.length > 1) {
        popFX();
    }
    isComposerNeeded();
}

/**
 * Adds an effect to the composer for post-processing visual effects.
 *
 * @param {FX} effect - Effect type: BLOOM, LUMINOSITY, GLITCH, SOBEL, HALFTONE.
 * @param {...any} params - Additional parameters for the effect, if required.
 * @returns {any} The effect pass that was added to the composer.
 * 
 * @example
 * // add a luminosity effect
 * pushFX(LUMINOSITY):
 * 
 * @example
 * // add a default bloom effect
 * pushFX(BLOOM);
 * 
 * @example
 * // add a custom bloom effect with threshold = 1.0, strength = 1.0, and radius = 0.5
 * pushFX(BLOOM, 1.0, 1.0, 0.5);
 * 
 * @example
 * // add a pixel effect with pixelSize = 10
 * pushFX(PIXEL, 10);
 * 
 * @example
 * // add a halftone effect with radius = 9.5, shape = HALFTONE_SQUARE, scattered = 0, grayscale = true
 * pushFX(HALFTONE, 9.5, HALFTONE_SQUARE, 0, true);
 
 * @example
 * // add a halftone effect with other available shapes like HALFTONE_DOT, HALFTONE_ELLIPSE and HALFTONE_LINE
 * pushFX(HALFTONE, 12, HALFTONE_LINE);
 * 
 * 
 */

const pushFX = (effect = LUMINOSITY, ...params) => {
    if (!composer) {
        const renderScene = new RenderPass(scene, cam);

        composer = new EffectComposer(renderer);
        composer.addPass(renderScene);
    }
    var addedPass;
    const canvasSizeAsVector = new THREE.Vector2(p5.instance.width, p5.instance.height)
    if (effect == GLITCH) {
        const glitchPass = new GlitchPass();
        composer.addPass(glitchPass);
        addedPass = glitchPass;
    } else if (effect == LUMINOSITY) {
        const effectGrayScale = new ShaderPass(LuminosityShader);
        composer.addPass(effectGrayScale);
        addedPass = effectGrayScale;
    } else if (effect == SOBEL) {
        const effectSobel = new ShaderPass(SobelOperatorShader);
        effectSobel.uniforms['resolution'].value = canvasSizeAsVector;
        effectSobel.uniforms['resolution'].value.multiplyScalar(window.devicePixelRatio);
        composer.addPass(effectSobel);
        addedPass = effectSobel;
    } else if (effect == BLOOM) {
        const bloomPass = params.length > 0 ? new UnrealBloomPass(canvasSizeAsVector, ...params) : new UnrealBloomPass(canvasSizeAsVector, 1.3, 1, 0.35);
        composer.addPass(bloomPass);
        addedPass = bloomPass;
    } else if (effect == PIXEL) {
        var pixelPass = new ShaderPass(RenderPixelatedPass);
        pixelPass.uniforms['resolution'].value = canvasSizeAsVector;
        pixelPass.uniforms['resolution'].value.multiplyScalar(window.devicePixelRatio);
        pixelPass.uniforms['pixelSize'].value = params.length > 0 ? params[0] : 4;
        composer.addPass(pixelPass);
        addedPass = pixelPass;
    } else if (effect == HALFTONE) {

        const halftoneparams = params && params.length > 0 ? {
            radius: params[0] ? params[0] : 4,
            scatter: params[2] ? params[2] : 0,
            shape: params[1] ? params[1] : HALFTONE_DOT,
            greyscale: params[3] ? params[3] : false,
            rotateR: params[4] ? radians(params[4]) : Math.PI / 12,
            rotateB: params[5] ? radians(params[5]) : Math.PI / 12 * 2,
            rotateG: params[6] ? radians(params[6]) : Math.PI / 12 * 3,
            blending: params[7] ? params[7] : 1,
            blendingMode: params[8] ? params[8] : 1

        } : {};
        const halftonePass = new HalftonePass(window.innerWidth, window.innerHeight, halftoneparams);
        composer.addPass(halftonePass);
        addedPass = halftonePass;
    }
    //TODO
    // else if (effect == DEPTH) {
    //     const ssaoPass = new SSAOPass(getScene(), getCamera(), width, height);
    //     ssaoPass.output = SSAOPass.OUTPUT.Depth;
    //     composer.passes[0].enabled = false;
    //     composer.addPass(ssaoPass);
    //     addedPass = ssaoPass;
    // }

    return addedPass;
}

const popFX = () => {
    if (composer.passes.length > 1) {
        composer.removePass(composer.passes[composer.passes.length - 1])
    }
    isComposerNeeded();
}

const getFX = (index) => {
    var selectedPass = index < composer.passes.length && index > 0 ? composer.passes[index] : null;
    return selectedPass;
}

const removeFX = (index) => {
    if (index < composer.passes.length && index > 0) {
        composer.removePass(composer.passes[index])
    }
    isComposerNeeded();
}

const isComposerNeeded = () => {
    if (composer.passes.length <= 1) {
        composer.dispose();
        composer = null;
    }
}

/**
 * 
 * Use getOrbit().disable() to block the camera view. getOrbit().enable() get back mouse controlled view.
 * 
 * @returns Camera orbit controls
 */

const getOrbit = () => {
    return orbit;
}

const color3D = (...args) => {
    var cTemp = p5.instance.color(...args);
    return new Color(cTemp.toString('rgb'));
}
/**
 * Generate a fully saturated random color or a random color with min/max bounds.
 * For example you may want a random color per object so you can call <pre>fill(randomColor())</pre>
 * 
 * 
 * @param {number} minHue Default 0
 * @param {number} maxHue Default 360
 * @param {number} minSaturation Default 100
 * @param {number} maxSaturation Default 100
 * @param {number} minLightness Default 50
 * @param {number} maxLightness Default 50
 * @returns {Color} - Random fully saturated color or as specificed by parameters
 */

const randomColor = (minHue = 0, maxHue = 360, minSaturation = 100, maxSaturation = 100, minLightness = 50, maxLightness = 50) => {
    const colorString = `hsl(${Math.round(p5.instance.random(minHue, maxHue))}, ${Math.round(p5.instance.random(minSaturation, maxSaturation))}%, ${Math.round(p5.instance.random(minLightness, maxLightness))}%)`;
    return p5.instance.color(colorString);
}

/**
 * Set the background color or texture of your 3D scene
 * @param {Color|Image} colorOrImage - A color representation like 255, 0, 255 or "#ff00ff", or an Image object.
 */

const background3D = (...args) => {

    if (args && args[0].drawingContext) {
        noSkybox();
        getScene().background = getTextureFromImage(args[0]);
        return
    }

    getScene().background = null;
    const newColor = color3D(...args);
    renderer.setClearColor(newColor);
    const alpha = p5.instance.alpha(p5.instance.color(...args));

    renderer.setClearAlpha(p5.instance.map(alpha, 0, 255, 0.0, 1.0));
    return newColor;
};


const equirectangularToCubemap = (equirectangular, size = 512) => {
    const cubeScene = new THREE.Scene();


    const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(size, {
        format: THREE.RGBFormat,
        generateMipmaps: true,
        minFilter: THREE.LinearMipmapLinearFilter
    });
    const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);
    cubeScene.add(cubeCamera);
    const material = new THREE.MeshBasicMaterial({
        map: equirectangular,
        side: THREE.BackSide
    });

    const geometry = new THREE.SphereGeometry(100, 100, 100);
    const mesh = new THREE.Mesh(geometry, material);
    cubeScene.add(mesh);

    cubeCamera.update(getRenderer(), cubeScene);

    const cubeMap = cubeCamera.renderTarget.texture;

    return cubeMap
};

/**
 * Sets the skybox background of the 3D scene using an equirectangular image.
 * If no equirectangularImage is provided, checks if a skybox background is already set.
 *
 * @param {Image} equirectangularImage - An equirectangular image used to create the skybox background.
 * @returns {Image} - The skybox background texture.
 */

const skybox = (equirectangularImage, attachToGrid = true, height = 20, radius = 200) => {
    if (!equirectangularImage) {
        return getScene().background != null && getScene().background.isCubeTexture
    }
    var equi;
    if (equirectangularImage.isCubeTexture) {
        equi = equirectangularImage;
    } else if (isImage(equirectangularImage)) {
        equi = getTextureFromImage(equirectangularImage);
    } 

    if (attachToGrid) {
        var env = new GroundProjectedEnv(equi, {
            height: height,
            radius: radius
        });
        env.scale.setScalar(100);
        if (currentGroundProjectedEnv) {
            scene.remove(currentGroundProjectedEnv);
        }

        currentGroundProjectedEnv = env;
        scene.add(currentGroundProjectedEnv);
    } else {
        if (currentGroundProjectedEnv) {
            scene.remove(currentGroundProjectedEnv);
        }
        getScene().background = equirectangularToCubemap(equi);
    }
    return equi
}

const noSkybox = () => {
    if (currentGroundProjectedEnv) {
        scene.remove(currentGroundProjectedEnv);
    }
    getScene().background = null;
}

/**
 * Set the color of your next 3D objects
 * 
 * @param {Color} color - A color rapresentation like 255, 0, 255 or "#ff00ff"
 */
const diffuse = (...args) => {
    currentColor = color3D(...args);
    return currentColor.clone();
}

/**
 * Get currently used diffuse color
 * 
 * @returns {Color} current diffuse color
 */
const getDiffuse = () => {
    return currentColor.toString();
}

const getTextureFromImage = (img) => {
    const ctx = img.drawingContext;
    return new CanvasTexture(ctx.canvas)
}

const noEnvMap = () => {
    currentEnvMap = null;
}

/**
 * Stop using a diffuse map when creating objects.
 */

const noDiffuseMap = () => {
    currentDiffuseMap = null;
}

const noRoughnessMap = () => {
    currentRoughnessMap = null;
}

const noMetalnessMap = () => {
    currentMetalnessMap = null;
}

const noDisplacementMap = () => {
    currentDisplacementMap = null;
}

const noNormalMap = () => {
    currentNormalMap = null;
}

const noBumpMap = () => {
    currentBumpMap = null;
}
/**
 * Creates a new point light with the specified color, position, and other parameters,
 * and adds it to the current scene.
 *
 * @param {Color} [color="#ffffff"] - The color of the light, in hexadecimal format.
 * @param {number} [x=0] - The X coordinate of the light's position.
 * @param {number} [y=0] - The Y coordinate of the light's position.
 * @param {number} [z=0] - The Z coordinate of the light's position.
 * @param {number} [intensity=1.0] - The intensity of the light.
 * @param {number} [distance=0] - The maximum distance from the light at which objects
 * should still be illuminated.
 * @param {number} [decay=2] - The rate at which the light's intensity decays over distance.
 *
 * @returns {PointLight} The newly created point light.
 * @example
 * 

 // Add a light to a scene with or without environment

function setup() {
  createCanvas3D(600, 400);
  background3D("#333333");
  //If you want only your lights you must use 
  //noEnvironment()
  environment(STARS)
  var lightX = 4;
  var lightY = 2;
  var lightZ = 2;
  var lightIntensity = 0.5; //don't exagerate๐Ÿ’ก
  
  pointLight("#9C27B0", lightX, lightY, lightZ, 0.5);

  diffuse("white");
  sphere(0, 0, 0, 1);
}

function draw() {}

 */

const pointLight = (color = "#ffffff", x = 0, y = 0, z = 0, intensity = 1.0, distance = 0, decay = 2) => {
    const lightColor = color3D(color);
    const newLight = new PointLight(lightColor, intensity, distance, decay);
    newLight.position.set(x, y, z)
    scene.add(newLight);
    return newLight;
}

/**
 * Add an ambient light.
 * 
 * @param {Color} color Color of the light
 * @param {Number} intensity 
 * @returns Newly created light object
 */

const ambientLight = (color = "#ffffff", intensity = 1.0) => {
    const lightColor = color3D(color);
    const newLight = new AmbientLight(lightColor, intensity);

    scene.add(newLight);
    return newLight;
}

const envMap = (img) => {
    currentEnvMap = getTextureFromImage(img);
    currentEnvMap.mapping = THREE.EquirectangularReflectionMapping;
    currentEnvMap.encoding = THREE.sRGBEncoding;
}
/**
 * Use an image as texture for all following 3D objects.
 * Use noDiffuseMap() to stop using a texture
 * 
 * @param {p5.Image} img
 * 
 * @example
 var img; // ๐Ÿ” You need a "global variable" when loading images

function preload() {
  // ๐Ÿงฑ always preload your textures
  img = loadImage("texture.png");
}

function setup() {
  createCanvas3D(600, 400);
  //Change background color of the scene
  background3D("#333333");
  
  
  //Use img as texture for following objects
  diffuse("white"); //Texture as it is needs white diffuse color
  diffuseMap(img);
  
  sphere(0, 0, 0, 1);
  
  noDiffuseMap();// Remove texture
  diffuse("#2196F3"); //Use a diffuse color as usual
  sphere(2.5, 0, 0, 1);
  
  diffuse("orange"); //Color your texture of orange
  diffuseMap(img);
  sphere(-2.5, 0, 0, 1);
}

function draw() {
  // ๐Ÿ” Never create objects here, but don't forget to have it in your code:
  // leave empty for the moment
}
 */
const diffuseMap = (img) => {
    currentDiffuseMap = getTextureFromImage(img);
}

const roughnessMap = (img) => {
    currentRoughnessMap = getTextureFromImage(img);
}

const ambientOcclusionMap = (img, intensity = 1.0) => {
    currentAOMap = getTextureFromImage(img);
    currentAOMapIntensity = intensity;
}


const metalnessMap = (img) => {
    currentMetalnessMap = getTextureFromImage(img);
}

const displacementMap = (img, scale = 1.0) => {
    currentDisplacementScale = scale;
    currentDisplacementMap = getTextureFromImage(img);
}

const bumpMap = (img, scale = 1.0) => {
    currentBumpScale = scale;
    currentBumpMap = getTextureFromImage(img);

}
const normalMap = (img) => {
    currentNormalMap = getTextureFromImage(img);
}

const exposure = (value) => {
    getRenderer().toneMappingExposure = value;
    getRenderer().needsUpdate = true;
}

const getEnvironment = () => {
    return scene.environment;
}

const noEnvironment = () => {
    scene.environment = null;
}

/**
 * Checks if a parameter is a p5.Image object
 * @param {*} img - The object to check
 * @returns {boolean} - true if the object is a p5.Image, otherwise false
 */
const isImage = (img) => {
    return (img && img.canvas && img.canvas instanceof HTMLCanvasElement);
};


const environment = (input, attachToGrid = true, background = true, height, radius) => {
    var texture;

    if (!input || typeof input == "boolean") {
        scene.environment = defaultEnvironment;
        texture = defaultEnvironment;
        background = input;
    } if (isImage(input)) {
        const equirectangular = getTextureFromImage(input);
        texture = equirectangularToCubemap(equirectangular);
    } else if (input) {
        const loader = new THREE.CubeTextureLoader();

        texture = input.isTexture ? input : loader.load([
            `${input}px.png`,
            `${input}nx.png`,
            `${input}py.png`,
            `${input}ny.png`,
            `${input}pz.png`,
            `${input}nz.png`,
        ]);
    }

    scene.environment = texture;

    if (background) {
        skybox(texture, attachToGrid, height, radius);
    }
    renderer.needsUpdate = true;

    return texture;
}

var currentGroundProjectedEnv;

const getDefaultShape = (radius, sides) => {
    const pts1 = [], count = sides;

    for (let i = 0; i < count; i++) {

        const l = radius;

        const a = 2 * i / count * Math.PI;

        pts1.push(new THREE.Vector2(Math.cos(a) * l, Math.sin(a) * l));

    }

    return new THREE.Shape(pts1);
}

const beginShape2D = () => {
    if (vertexContext != NONE) {
        console.warn("You must use endShape2D() before using beginShape2D() again!");
        return;
    }

    vertexContext = SHAPING;
    const newShape = new Shape();
    newShape.justCreated = true;
    currentShape = newShape;
    return newShape;
}

const endShape2D = () => {
    vertexContext = NONE;
    return currentShape;
}

const addVertex = (x, y, z) => {
    if (vertexContext == NONE) {
        console.warn("addVertex() can be used only inside beginShape2D() and endShape2D() or beginExtrude() and endExtrude() or beginLathe() and endLathe() ");
        return;
    }
    const newPoint = new Vector3(x, y, z);

    if (vertexContext == LATHING) {
        currentLathe.push(newPoint)
    } else if (vertexContext == EXTRUDING) {
        currentExtrude.push(newPoint)
    } else if (vertexContext == SHAPING) {
        if (currentShape.justCreated) {
            currentShape.justCreated = false;
            currentShape.moveTo(x, y);
        } else {
            currentShape.lineTo(x, y);
        }
    }
}

const beginLathe = (sides = 32) => {
    if (vertexContext != NONE) {
        console.warn("You must use endLathe() before using beginLathe() again!");
        return;
    }
    vertexContext = LATHING;
    currentLatheSides = sides;
    currentLathe = [];
}

const endLathe = () => {
    if (!vertexContext == LATHING) {
        console.warn("You must use beginLathe() before using endLathe(); Cannot be used inside beginShape2D() endShape2D()!");
        return;
    }
    const geometry = new THREE.LatheGeometry(currentLathe, currentLatheSides);
    vertexContext = NONE;
    return addMesh(0, 0, 0, geometry);

}

const beginExtrude = (shape, sides = 32) => {
    if (vertexContext != NONE) {
        console.warn("You must use endExtrude() before using beginExtrude() again!");
        return;
    }

    if (!shape) {
        shape = getDefaultShape(0.5, sides);
    } else if (shape && !shape.extractPoints) { //
        const radius = shape;
        shape = getDefaultShape(radius, sides);
    } else if (shape && shape.extractPoints) {
        currentExtrudeSteps = sides;
    }

    //console.warn("EXPERIMENTAL: extrude methods may be subject to changes!");

    vertexContext = EXTRUDING;
    currentExtrudeShape = shape;
    currentExtrude = [];
}

/**
 * Creates a 3D mesh based on a shape, with options for depth and beveling.
 *
 * @param {Shape} shape - The shape to use for creating the mesh.
 * @param {number} x - The x position of the mesh.
 * @param {number} y - The y position of the mesh.
 * @param {number} z - The z position of the mesh.
 * @param {number} [depth=0] - The depth of the mesh. If not specified, a 2D shape will be used.
 * @param {boolean} [bevelEnabled=false] - Specifies whether the mesh should have a bevel.
 * @param {number} [bevelThickness] - The thickness of the bevel. If not specified, it is calculated based on the depth.
 * @param {number} [bevelSize] - The size of the bevel. If not specified, it uses the same value as `bevelThickness`.
 * @param {number} [bevelOffset=0] - The offset of the bevel from the surface of the mesh.
 * @param {number} [bevelSegments=6] - The number of segments used to create the bevel.
 *
 * @returns {Mesh3D} The mesh created from the specified shape.
 */


const shape = (shape, x, y, z, depth = 0, bevelEnabled = false, bevelThickness, bevelSize, bevelOffset = 0, bevelSegments = 6) => {
    if (depth == 0) {
        const geometry = new THREE.ShapeGeometry(shape);
        geometry.rotateX(-Math.PI / 2);
        geometry.translate(0, -depth / 2, 0);
        return addMesh(x, y, z, geometry);
    } else {
        if (!bevelThickness)
            bevelThickness = depth * 0.1;

        const extrudeSettings = {
            steps: 12,
            depth: depth,
            bevelEnabled: bevelEnabled,
            bevelThickness: bevelThickness,
            bevelSize: bevelSize ? bevelSize : bevelThickness,
            bevelOffset: bevelOffset,
            bevelSegments: bevelSegments
        };
        const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);//BufferGeometryUtils.mergeVertices();
        geometry.rotateX(-Math.PI / 2);
        geometry.translate(0, -depth / 2, 0);

        return addMesh(x, y, z, geometry);
    }

}

var currentExtrudeSteps = 12; //TODO
const endExtrude = (closed = false) => {

    if (!vertexContext == EXTRUDING) {
        console.warn("You must use beginExtrude() before using endExtrude(); Cannot be used inside beginShape2D() endShape2D()!");
        return;
    }
    vertexContext = NONE;

    const closedSpline = new THREE.CatmullRomCurve3(currentExtrude);

    closedSpline.curveType = 'catmullrom';
    closedSpline.closed = closed;
    const extrudeSettings = {
        steps: currentExtrude.length,
        bevelEnabled: false,
        extrudePath: closedSpline
    };

    const geometry = BufferGeometryUtils.mergeVertices(new THREE.ExtrudeGeometry(currentExtrudeShape, extrudeSettings));

    currentExtrude = null;
    const oldAlign = currentAlign;
    align('center');
    const newMesh = addMesh(0, 0, 0, geometry);
    align(oldAlign);


    return newMesh;
}

const beginGroup = () => {
    const newGroup = new Group3D();

    groupStack.push(newGroup)
    currentScene = groupStack[groupStack.length - 1];

    return currentScene;
}

const endGroup = () => {
    const groupToEnd = groupStack.pop();
    if (groupStack.length > 0) {
        currentScene = groupStack[groupStack.length - 1];
    } else {
        currentScene = scene;
    }


    currentScene.add(groupToEnd);
    return groupToEnd;
}

const quality = (preset) => {
    if (preset === FULL_HD) {
        setQuality(8, 8, 8, 8, 8, 8, 8, 8);
    } else if (preset === HD) {
        setQuality(2, 2, 2, 2, 2, 2, 2, 2);
    } else if (preset === SD) {
        setQuality(1, 1, 1, 1, 1, 1, 1, 1);
    } else if (preset === LD) {
        setQuality(0.5, 0.5, 0.5, 0.5, 0.5, 2, 0.3, 0.5);
    }
}

const flatShading = () => {
    currentShading = FLAT;
}

const smoothShading = () => {
    currentShading = SMOOTH;
}

const getLast = () => {
    return last
}

const align = (...align) => {
    currentAlign = align;
}

const alignMesh = (mesh, aligns) => {
    var bbox = new THREE.Box3();
    var cent = bbox.getCenter(new THREE.Vector3());
    var size = bbox.getSize(new THREE.Vector3());
    bbox.setFromObject(mesh);
    bbox.getCenter(cent);
    bbox.getSize(size);
    var x = 0;
    var y = 0;
    var z = 0;
    for (let align of aligns)
        if (align == 'top') {
            y += (size.y * .5);
        } else if (align == 'bottom') {
            y -= (size.y * .5);
        } else if (align == 'left') {
            x -= (size.x * .5);
        } else if (align == 'right') {
            x += (size.x * .5);
        } else if (align == 'forward') {
            z -= (size.z * .5);
        } else if (align == 'backward') {
            z += (size.z * .5);
        }

    mesh.geometry.translate(x, y, z);
}

const subdivide = (subdivisions = 1, split = true, uvSmooth = false, preserveEdges = false, flatOnly = false, maxTriangles = Infinity) => {
    currentSubdivisions = {
        subdivisions: min(5, max(0, floor(subdivisions))),
        split: split,
        uvSmooth: uvSmooth,
        preserveEdges: preserveEdges,
        flatOnly: flatOnly,
        maxTriangles: maxTriangles
    };
    notify("You are using a very experimental function");
}

const addMesh = (x, y, z, geometry, addToScene = true) => {
    if (!geometry)
        return;

    const newMesh = new Mesh3D(geometry, currentMaterial.clone());
    newMesh.material.side = DoubleSide;

    //if (currentNormalMap)
    newMesh.material.normalMap = currentNormalMap;

    newMesh.material.map = currentDiffuseMap;// ? currentDiffuseMap : null;

    newMesh.material.roughnessMap = currentRoughnessMap;// ? currentRoughnessMap : null;
    // if (currentRoughnessMap)
    //     newMesh.material.roughness = 1.0;

    // if (currentAOMap) {
    newMesh.material.aoMap = currentAOMap;
    newMesh.material.aoMapIntensity = currentAOMapIntensity;

    // }
    newMesh.material.metalnessMap = currentMetalnessMap;// ? currentMetalnessMap : null;
    // if (currentMetalnessMap) {
    //     newMesh.material.metalness = 1.0;
    // }
    newMesh.material.envMap = currentEnvMap;// ? currentEnvMap : null;
    newMesh.material.displacementMap = currentDisplacementMap;// ? currentDisplacementMap : null;
    //if (currentDisplacementMap) {

    newMesh.material.displacementScale = currentDisplacementScale;
    //}
    newMesh.material.bumpMap = currentBumpMap ? currentBumpMap : null;
    //if (currentBumpMap) {

    newMesh.material.bumpScale = currentBumpScale;
    //}
    newMesh.material.color = currentColor;
    newMesh.material.flatShading = currentShading;

    newMesh.material.needsUpdate = true;


    newMesh.position.set(x, y, z);

    alignMesh(newMesh, currentAlign);

    if (addToScene)
        currentScene.add(newMesh);


    if (currentSubdivisions.subdivisions > 0) {
        newMesh.subdivide(currentSubdivisions.subdivisions, currentSubdivisions.split, currentSubdivisions.uvSmooth, currentSubdivisions.preserveEdges, currentSubdivisions.flatOnly, currentSubdivisions.maxTriangles)
    }

    last = newMesh;
    return newMesh;
}

/**
 * Remove one or more Mesh3D or Group3D.
 * 
 * @param  {...Mesh3D|Group3D} objects3D meshes or groups you want to remove
 */

const removeObjects = (...objects3D) => {
    for (let o of objects3D) {
        if (o.parent)
            o.parent.remove(o);
    }
}


/**
 * Set the emissive color of your next 3D objects
 * 
 * @param {Color} color - A color rapresentation like 255, 0, 255 or "#ff00ff"
 */

const emissive = (color) => {
    currentMaterial.emissive = color3D(color);
};

/**
 * Set the emissive intesity of your next 3D object
 * 
 * @param {number} intensity - A value between 0.0 and 1.0
 */
const emissiveIntensity = (intensity = 1.0) => {
    currentMaterial.emissiveIntensity = intensity;
};

/**
 * Set the roughness of your next 3D object
 * 
 * @param {number} roughness - A value between 0.0 and 1.0
 */
const roughness = (roughness = 1.0) => {
    currentMaterial.roughness = roughness;
}

/**
 * Set the opacity of your next 3D object
 * 
 * @param {number} opacity - A value between 0.0 and 1.0
 */
const opacity = (opacity = 1.0) => {
    currentMaterial.opacity = opacity;

    if (currentMaterial.opacity < 1)
        currentMaterial.transparent = true;
    else
        currentMaterial.transparent = false;
}

/**
 * Set the metalness of your next 3D object
 * 
 * @param {number} metalness - A value between 0.0 and 1.0
 */
const metalness = (metalness = 0.0) => {
    currentMaterial.metalness = metalness;
}
/**
 * Get active metalness value
 * @returns {number} Current material metalness value between 0.0 and 1.0
 */
const getMetalness = () => {
    return currentMaterial.metalness;
}

/**
 * Get active opacity value
 * @returns {number} Current material opacity value between 0.0 and 1.0
 */
const getOpacity = () => {
    return currentMaterial.opacity;
}

/**
 * Get active roughness value
 * @returns {number} Current material roughness value between 0.0 and 1.0
 */

const getRoughness = () => {
    return currentMaterial.roughness;
}
/**
 * Get active emissive color
 * @returns {Color} - Current material emissive color
 */
const getEmissive = () => {
    return currentMaterial.emissive.clone();
}

/**
 * Get active emissiveIntensity value
 * @returns {number} Current material emissiveIntensity value between 0.0 and 1.0
 */
const getEmissiveIntensity = () => {
    return currentMaterial.emissiveIntensity;
}
/**
 * Get a copy of active material
 * @returns {Material} A copy of current material
 */
const getMaterial = () => {
    const material = currentMaterial.clone();
    material.color = currentColor.clone();
    material.flatShading = currentShading;
    return material;
}

/**
 * 
 * Quickest way of setting a material.
 * 
 * @param  {Color|Material} material This can be a color or another material
 * @param  {number} roughness Optional roughness. Default 1.0
 * @param  {number} opacity Optional opacity. Default 1.0
 * @param  {number} metalness Optional metalness. Default 0.0
 * @param  {Color} emissive Optional emissive color. Default is black (no emissive color)
 * @param  {number} emissiveIntensity Optional emissiveIntensity. Default 1.0
 * @returns {Material} newly created material
 */
const material = (...params) => {

    if (params.length == 0)
        return getMaterial();

    currentMaterial = new MeshStandardMaterial();
    if (params.length == 1) {
        if (params[0].isMeshStandardMaterial) {
            currentMaterial = params[0].clone();
            currentColor = params[0].color.clone();
            currentShading = params[0].flatShading;
        } else {
            fill(params[0]);
        }
    }
    if (params.length > 1) {
        fill(params[0]);
        roughness(params[1]);
    }
    if (params.length > 2)
        opacity(params[2]);

    if (params.length > 3)
        metalness(params[3]);

    if (params.length > 4)
        emissive(params[4]);

    if (params.length > 5)
        emissiveIntensity(params[5]);

    return getMaterial();
}

/**
 * Add a clone of a Mesh3D or a Group to new position.
 * 
 * Cloning a Mesh3D will take into account current material in use. For example fill("red") before cloning
 * would change the color of the cloned Mesh3D.
 * 
 * Group3D objects maintain their original material.
 * 
 * @param {Mesh3D|Group3D} object Object or Group you want to clone
 * @param {number} x Default 0.0
 * @param {number} y Default 0.0
 * @param {number} z Default 0.0
 * @returns {Mesh3D|Group3D} cloned Object or Group depending on input
 */
const clone = (object, x = 0, y = 0, z = 0) => {
    if (object.isGroup || object.isMesh) {
        const clonedObject = SkeletonUtils.clone(object);
        clonedObject.position.set(x, y, z);
        currentScene.add(clonedObject);
        return clonedObject

    } else {
        notify("clone() works only with groups or mesh!", REASON_CANNOT);
        return null;
    }


}

const getCenteredMesh = (result) => {
    const oldAlign = currentAlign;
    align('center');
    const newMesh = addMesh(result.position.x, result.position.y, result.position.z, result.geometry);
    align(oldAlign);
    return newMesh
}

/**
 * 
 * Create a new object subtracting a from b.
 * 
 * @param {Mesh3D} a Source object
 * @param {Mesh3D} b Object you want to subtract from source
 * @param {boolean} removeOriginals Use false if you want to mantain original object in the scene. Default true
 * @returns {Mesh3D} newly created object
 */
const subtract = (source, ...targets) => {
    var result = source;
    removeObjects(source)
    for (let target of targets) {
        result = booleanOperation(result, target, 'subtract');
        removeObjects(target);
    }

    return getCenteredMesh(result);
}

/**
 * 
 * Create a new object merging a and b togheter.
 * 
 * @param {Mesh3D} a Source object
 * @param {Mesh3D} b Object you want to merge to source
 * @param {boolean} removeOriginals Use false if you want to mantain original object in the scene. Default true
 * @returns {Mesh3D} newly created object
 */
const union = (source, ...targets) => {
    var result = source;
    removeObjects(source)
    for (let target of targets) {
        result = booleanOperation(result, target, 'union');
        removeObjects(target);
    }

    return getCenteredMesh(result);
}

/**
 * 
 * Create a new object from the intersection between a and b.
 * 
 * @param {Mesh3D} a Source object
 * @param {Mesh3D} b Object you want to intersect with source
 * @param {boolean} removeOriginals Use false if you want to mantain original object in the scene. Default true
 * @returns {Mesh3D} newly created object
 */
const intersect = (source, ...targets) => {
    var result = source;
    removeObjects(source)
    for (let target of targets) {
        result = booleanOperation(result, target, 'intersect');
        removeObjects(target);
    }

    return getCenteredMesh(result);
}

/**
 * Create a plane with an image as texture
 * @param {*} img Texture image (Remember to load your images inside preload())
 * @param {*} x  Position x
 * @param {*} y  Position y
 * @param {*} z  Position z
 * @param {*} width Width of the plane. You may want to use a multiple of img.width to avoid stretching. Default 1.0
 * @param {*} height Height of the plane. You may want to use a multiple of img.height to avoid stretching. Default 1.0
 * @returns {Mesh3D} 3D plane with the image as texture
 */

const image3D = (img, x, y, z, width, height) => {
    const map = getTextureFromImage(img);
    const aspectRatio = img.width / img.height;
    if (!height) {
        width = width ? width : 1;
        height = width / aspectRatio;
        //console.log(aspectRatio)
    }

    const p = plane(x, y, z, width, height);

    p.material.map = map;
    p.material.depthWrite = false;
    p.material.transparent = true;
    p.material.alphaTest = 0.01;
    //p.material.depthTest = false;
    p.material.needsUpdate = true;
    return p;
}

/**
 * Add a sphere to the scene.
 * 
 * @param {number} x X position
 * @param {number} y Y position
 * @param {number} z Z position
 * @param {number} radius Optional radius of the sphere. Default 0.5
 * @param {number} phiStart 
 * @param {number} phiLength 
 * @param {number} thetaStart 
 * @param {number} thetaLength 
 * @returns {Mesh3D} newly created sphere object
 */
const sphere = (x = 0, y = 0, z = 0, radius = 0.5, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI) => {
    if (radius == 0) {
        notify("You tried to create a sphere with radius = 0. No sphere was created.", REASON_CHECK);
        return
    }
    const geometry = getGeometry({ type: 'sphere', radius: radius, phiStart: phiStart, phiLength: phiLength, thetaStart: thetaStart, thetaLength: thetaLength });
    return addMesh(x, y, z, geometry);
}

const circle = (x = 0, y = 0, z = 0, radius = 0.5) => {
    const geometry = getGeometry({ type: 'circle', radius: radius });
    return addMesh(x, y, z, geometry);
}

const ring = (x = 0, y = 0, z = 0, outerRadius = 0.5, innerRadius = 0.2) => {
    const geometry = getGeometry({ type: 'ring', outerRadius: outerRadius, innerRadius: innerRadius });
    return addMesh(x, y, z, geometry);
}
/**
 * Add a cone to the scene.
 * 
 * @param {number} x X position
 * @param {number} y Y position
 * @param {number} z Z position
 * @param {number} radius Optional radius of the cone. Default 0.5
 * @param {number} height Optional height of the cone. Default 1
 * @returns {Mesh3D} newly created cone object
 */
const cone = (x = 0, y = 0, z = 0, radius = 0.5, height = 1) => {
    if (radius == 0) {
        notify("You tried to create a cone with radius = 0. No cone was created.", REASON_CHECK);
        return
    }
    const geometry = getGeometry({ type: 'cone', radius: radius, height: height });
    return addMesh(x, y, z, geometry);
}
/**
 * Add a capsule to the scene.
 * 
 * @param {number} x X position
 * @param {number} y Y position
 * @param {number} z Z position
 * @param {number} radius Optional radius of the capsule. Default 0.5
 * @param {number} length Optional length of the capsule. Default 1
 * @param {number} capSubdivisions Optional number of cap subdivisions of the capsule. Default 32
 * @param {number} radialSegments Optional number of radial segments of the capsule. Default 12
 * @returns {Mesh3D} newly created cone object
 */
const capsule = (x = 0, y = 0, z = 0, radius = 0.5, length = 1, capSubdivisions, radialSegments) => {
    if (radius == 0) {
        notify("You tried to create a capsule with radius = 0. No capsule was created.", REASON_CHECK);
        return
    }
    const geometry = getGeometry({ type: 'capsule', radius: radius, length: length, radialSegments: radialSegments, capSubdivisions: capSubdivisions });
    return addMesh(x, y, z, geometry);
}
/**
 * 
 * @param {number} x  X position
 * @param {number} y  Y position
 * @param {number} z  Z position
 * @param {number} radiusTop Optional radius of the cylinder. Default 0.5
 * @param {number} height Optional height of the cylinder. Default 1
 * @param {number} radialSegments Optional number of radial segments of the cylinder. Default 32
 * @param {number} radiusBottom Optional bottom radius of the cylinder. Default null (egual to radiusTop)
 * @param {number} heightSegments Optional number of height segments of the cylinder. Default 12
 * @returns  {Mesh3D} newly created object
 */
const cylinder = (x = 0, y = 0, z = 0, radiusTop = 0.5, height = 1, radialSegments, radiusBottom, heightSegments) => {
    if (radiusTop == 0) {
        notify("You tried to create a cylinder with radius = 0. No cylinder was created.", REASON_CHECK);
        return
    }
    const geometry = getGeometry({ type: 'cylinder', radiusTop: radiusTop, radiusBottom: radiusBottom ? radiusBottom : radiusTop, height: height, radialSegments: radialSegments, heightSegments: heightSegments });
    return addMesh(x, y, z, geometry);
}
/**
 * Create a cube object.
 * @param {number} x X position
 * @param {number} y Y position
 * @param {number} z Z position
 * @param {number} side Side length
 * @returns {Mesh3D}  newly created object
 */
const cube = (x, y, z, side) => {
    return box(x, y, z, side, side, side);
}

const plane = (x = 0, y = 0, z = 0, width = 1, height = 1, widthSegments = 1, heightSegments = 1) => {
    const geometry = getGeometry({ type: 'plane', width: width, height: height, widthSegments: widthSegments, heightSegments: heightSegments });
    return addMesh(x, y, z, geometry);
}

/**
 * Create a box object.
 * 
 * @param {number} x 
 * @param {number} y 
 * @param {number} z 
 * @param {number} width 
 * @param {number} height 
 * @param {number} depth 
 * @returns {Mesh3D} newly created object
 */
const box = (x = 0, y = 0, z = 0, width = 1, height = 1, depth = 1) => {
    const geometry = getGeometry({ type: 'box', width: width, height: height, depth: depth });
    return addMesh(x, y, z, geometry);
}
/**
 * 
 * Create an ellipsoid object. It's like a box buf spherical ;-)
 * 
 * @param {number} x 
 * @param {number} y 
 * @param {number} z 
 * @param {number} width 
 * @param {number} height 
 * @param {number} depth 
 * @returns {Mesh3D} newly created object
 */
const ellipsoid = (x = 0, y = 0, z = 0, width = 1, height = 1, depth = 1) => {
    const geometry = getGeometry({ type: 'ellipsoid', width: width, height: height, depth: depth });
    return addMesh(x, y, z, geometry);
}
/**
 * Create a torus (like a donut)
 * 
 * @param {number} [x=0] - The x coordinate of the torus center.
 * @param {number} [y=0] - The y coordinate of the torus center.
 * @param {number} [z=0] - The z coordinate of the torus center.
 * @param {number} [radius=0.5] - The radius of the torus.
 * @param {number} [tubeRadius=0.2] - The radius of the tube of the torus.
 * @returns {Mesh3D} The newly created torus mesh object.
 */
const torus = (x = 0, y = 0, z = 0, radius = 0.5, tubeRadius = 0.2) => {
    if (radius == 0) {
        notify("You tried to create a torus with radius = 0. No torus was created.", REASON_CHECK);
        return
    }
    if (tubeRadius == 0) {
        notify("You tried to create a torus with tubeRadius = 0. No torus was created.", REASON_CHECK);
        return
    }
    const geometry = getGeometry({ type: 'torus', radius: radius, tube: tubeRadius });
    return addMesh(x, y, z, geometry);
}
/**
 * 
 * Create an icosahedron. Can be used to create spheres with more regular topology than the sphere()
 * 
 * @param {number} x 
 * @param {number} y 
 * @param {number} z 
 * @param {number} radius 
 * @param {number} detail Default is 0. More then 1 it's a sphere
 * @returns {Mesh3D} newly created object
 */
const icosahedron = (x = 0, y = 0, z = 0, radius = 0.5, detail = 0) => {
    if (radius == 0) {
        notify("You tried to create a icosahedron with radius = 0. No icosahedron was created.", REASON_CHECK);
        return
    }
    const geometry = getGeometry({ type: 'icosahedron', radius: radius, detail: detail });
    return addMesh(x, y, z, geometry);
}
/**
 * Create an octahedron.
 * 
 * @param {number} x 
 * @param {number} y 
 * @param {number} z 
 * @param {number} radius 
 * @param {number} detail 
 * @returns {Mesh3D} newly created object
 */
const octahedron = (x = 0, y = 0, z = 0, radius = 0.5, detail = 0) => {
    if (radius == 0) {
        notify("You tried to create a octahedron with radius = 0. No octahedron was created.", REASON_CHECK);
        return
    }
    const geometry = getGeometry({ type: 'octahedron', radius: radius, detail: detail });
    return addMesh(x, y, z, geometry);
}
/**
 * Create an tetrahedron.
 * 
 * @param {number} x 
 * @param {number} y 
 * @param {number} z 
 * @param {number} radius 
 * @param {number} detail 
 * @returns {Mesh3D} newly created object
 */
const tetrahedron = (x = 0, y = 0, z = 0, radius = 0.5, detail = 0) => {
    if (radius == 0) {
        notify("You tried to create a shape with radius = 0. No shape was created.", REASON_CHECK);
        return
    }
    const geometry = getGeometry({ type: 'tetrahedron', radius: radius, detail: detail });
    return addMesh(x, y, z, geometry);
}

const pyramid = (x = 0, y = 0, z = 0, width = 1, height = 1, depth = 1) => {
    const geometry = getGeometry({ type: 'pyramid', width: width, height: height, depth: depth });
    return addMesh(x, y - (height * 0.5), z, geometry);
}

const inspect = (something) => {
    if (inspector) {
        inspector.inspect(something);
    }
}

const lookAt = (target, y, z) => {
    if (target == undefined) {
        target = new Vector3();
    }

    if (target.isVector3)
        orbit.target = target.clone();
    else if (target.isObject3D) {

        if (target.parent != scene) {
            target.traverseAncestors((o) => {
                if (o.parent == scene) {
                    target = o;
                }
            })
        }

        orbit.target = target.position.clone();
    }
    else if (target != undefined && y != undefined && z != undefined) {
        orbit.target = new Vector3();
        orbit.target.set(target, y, z);
    }

    orbit.update();
}
/**
 * 
 * Use pickObject(x,y) to know which object is under x,y coords (pixel) of the view.
 * You can use mouseX, mouseY to evaluate the object under mouse cursor.
 * 
 * 
 * @param {Integer} x 
 * @param {Integer} y 
 * @returns Object is under x,y coords (pixel) of the view. null if there is no object rendered at those coords
 * @example
 * // Change the color of an object when clicked
 * function mousePressed() {
 *      var clickedObject = pickObject(mouseX, mouseY);
 *      
 *      if (clickedObject) {
 *       //implement what happen if you clicked on something
 *       clickedObject.setColor("red")
 *      } else {
 *       //no object under the mouse
 *      }
 * }
 */

const pickObject = (x, y) => {

    if (!x && !y && p5 && p5.instance) {
        x = p5.instance.mouseX;
        y = p5.instance.mouseY;
    }

    const currentObjectUnderMouse = scene.getObjectById(picker.pick(x - p5.instance.canvas.getBoundingClientRect().left, y - p5.instance.canvas.getBoundingClientRect().top));
    if (currentObjectUnderMouse && currentObjectUnderMouse.isMesh)
        return currentObjectUnderMouse;
}

const showToolbox = () => {
    toolboxContainer.style.display = 'initial'
}


const hideToolbox = () => {
    toolboxContainer.style.display = 'none'
}

/**
 * 
 * Use showGrid() to have the reference floor grid shown in your 3D scene. Use hideGrid() to hide it.
 * 
 * @param {Boolean} withGrid If specified it will show the grid with true and hide the grid with false. Default true
 */

const showGrid = (withGrid = true) => {
    scene.showGrid(withGrid);
}

/**
 * Hide the reference floor grid in your scene. Use showGrid() to enable it again.
 */

const hideGrid = () => {
    scene.showGrid(false);
}

const getScene = () => {
    return scene;
}

const getCamera = () => {
    return cam;
}


const setCamera = (...params) => {
    if (params.length >= 1 && params.length < 3) {
        cam.position.set(params[0].x, params[0].y, params[0].z);
        if (params.length == 2)
            orbit.target.set(params[1].x, params[1].y, params[1].z);
    } else if (params.length >= 3 && !Number.isNaN(params[0]) && !Number.isNaN(params[1]) && !Number.isNaN(params[2])) {

        cam.position.set(params[0], params[1], params[2]);
        if (params.length == 6 && !Number.isNaN(params[3]) && !Number.isNaN(params[4]) && !Number.isNaN(params[5])) {
            orbit.target.set(params[3], params[4], params[5]);
        }
    }
    cam.updateProjectionMatrix();
    orbit.update();
}

const changeCamera = (type = PERSPECTIVE, ...params) => {

    if (type == PERSPECTIVE) {
        cam == new PerspectiveCamera(
            75,
            p5.instance.width / p5.instance.height, //aspect
            0.1,
            1000
        );
        setCamera(CAMERA_DEFAULT_POSITION_PERSPECTIVE)
    } else if (type == ORTHOGRAPHIC) {
        cam = new THREE.OrthographicCamera(p5.instance.width / - 2, p5.instance.width / 2, p5.instance.height / 2, p5.instance.height / - 2, 0, 1000);
        setCamera(CAMERA_DEFAULT_POSITION_ORTHOGRAPHIC);
        fitZoom();
    }

    cam.updateProjectionMatrix();
    orbit = new OrbitControls(cam, p5.instance.canvas);
    orbit.update();
    return cam;
}


const getRenderer = () => {
    return renderer;
}

const setZoom = (zoom = 1) => {
    cam.zoom = zoom;
    cam.updateProjectionMatrix();
    orbit.update();
}


const fitZoom = (selection) => {
    const size = new THREE.Vector3();
    const center = new THREE.Vector3();
    const box = new THREE.Box3();

    if (!selection)
        selection = getScene().children;
    //For centering the meshGroup
    box.makeEmpty();
    if (selection.isGroup) {
        selection = selection.children;
    } else if (selection.isMesh) {
        selection = [selection]
    }
    for (const object of selection) {
        if (object.visible)
            box.expandByObject(object);
    }

    box.getSize(size);
    box.getCenter(center);
    //box.center(this.scene);
    //meshGroup.localToWorld(box);
    //meshGroup.position.multiplyScalar(-1);

    //For fitting the object to the screen when using orthographic camera
    const currentCamera = getCamera();
    if (currentCamera.isOrthographicCamera) {
        const fitOffset = 0.7;
        currentCamera.zoom = Math.min(width / (box.max.x - box.min.x),
            height / (box.max.y - box.min.y)) * fitOffset;

        var cameraPosition = CAMERA_DEFAULT_POSITION_ORTHOGRAPHIC.clone();
        currentCamera.position.set(currentCamera.position.x + center.x, currentCamera.position.y + center.y, currentCamera.position.z + center.z);
        orbit.target.set(center.x, center.y, center.z)
        currentCamera.updateProjectionMatrix();
        currentCamera.updateMatrix();
    } else if (currentCamera.isPerspectiveCamera) {
        const fitOffset = 1.2;
        const maxSize = Math.max(size.x, size.y, size.z);
        const fitHeightDistance = maxSize / (2 * Math.atan(Math.PI * currentCamera.fov / 360));
        const fitWidthDistance = fitHeightDistance / currentCamera.aspect;
        const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance);

        const direction = orbit.target.clone()
            .sub(currentCamera.position)
            .normalize()
            .multiplyScalar(distance);

        orbit.maxDistance = distance * 10;
        orbit.target.copy(center);

        currentCamera.near = distance / 100;
        currentCamera.far = distance * 100;
        currentCamera.updateProjectionMatrix();

        currentCamera.position.copy(orbit.target).sub(direction);

        orbit.update();
    }
}

const getZoom = () => {
    return cam.zoom
}

const createGradient = (colors, steps) => {
    let gradient = [];
    for (let i = 0; i < colors.length - 1; i++) {
        let startColor = color(colors[i]);
        let endColor = color(colors[i + 1]);
        for (let j = 0; j < steps; j++) {
            let interp = j / (steps - 1);
            gradient.push(lerpColor(startColor, endColor, interp));
        }
    }
    return gradient;
}

const VERTICAL = 'vertical';
const HORIZONTAL = 'horizontal';
const TILES = 'tiles';

const getGradient = (mode, steps, ...colors) => {
    notify("You are using a very experimental function");
    //const steps = 64;
    steps = steps ? floor(steps) : 32;
    const gradient = createGradient(colors, steps);
    const tempG = createGraphics(512, 512);
    tempG.ellipseMode(CORNER);
    tempG.background("magenta");

    tempG.noStroke();
    if (mode == VERTICAL) {
        for (let row = 0; row < tempG.height; row += tempG.height / steps) {
            tempG.fill(gradient[floor(map(row, 0, tempG.height, 0, gradient.length))])
            tempG.rect(0, row, tempG.width, floor(tempG.height / steps) + 2);
        }
    } else if (mode == HORIZONTAL) {
        for (let col = 0; col < tempG.width; col += tempG.width / steps) {
            tempG.fill(gradient[floor(map(col, 0, tempG.width, 0, gradient.length))])
            tempG.rect(col, 0, floor(tempG.width / steps) + 2, tempG.height);
        }
    } else if (mode == TILES) {
        for (let row = 0; row < tempG.height; row += tempG.height / steps)
            for (let col = 0; col < tempG.width; col += tempG.width / steps) {
                tempG.fill(gradient[floor(map(random() * tempG.width, 0, tempG.width, 0, gradient.length))])
                tempG.rect(col, row, floor(tempG.width / steps) + 2, floor(tempG.height / steps) + 2);
            }
    }
    //img.copy(tempG, 0, 0, tempG.width, tempG.height, 0, 0, img.width, img.height)
    return tempG;
}
p5.prototype.getGradient = getGradient;
p5.prototype.VERTICAL = VERTICAL;
p5.prototype.HORIZONTAL = HORIZONTAL;
p5.prototype.TILES = TILES;

p5.prototype.THREE = THREE;
p5.prototype.getScene = getScene;
p5.prototype.getCamera = getCamera;
p5.prototype.setZoom = setZoom;
p5.prototype.getZoom = getZoom;
p5.prototype.fitZoom = fitZoom;
p5.prototype.showToolbox = showToolbox;
p5.prototype.hideToolbox = hideToolbox;
p5.prototype.getOrbit = getOrbit;
p5.prototype.getRenderer = getRenderer;

p5.prototype.resizeCanvas3D = resizeCanvas3D;

p5.prototype.onScreen = onScreen;

p5.prototype.showGrid = showGrid;
p5.prototype.hideGrid = hideGrid;

p5.prototype.addMesh = addMesh;
p5.prototype.clone = clone;

p5.prototype.removeFromScene = removeObjects;
p5.prototype.removeObjects = removeObjects;
p5.prototype.removeObject = removeObjects;

p5.prototype.getLast = getLast;

p5.prototype.text3D = text3D;
p5.prototype.setFont3D = setFont3D;

p5.prototype.beginGroup = beginGroup;
p5.prototype.endGroup = endGroup;


p5.prototype.addVertex = addVertex;

p5.prototype.beginShape2D = beginShape2D;
p5.prototype.endShape2D = endShape2D;

p5.prototype.beginExtrude = beginExtrude;
p5.prototype.endExtrude = endExtrude;

p5.prototype.beginLathe = beginLathe;
p5.prototype.endLathe = endLathe;

p5.prototype.normalMap = normalMap;
p5.prototype.diffuseMap = diffuseMap;
p5.prototype.roughnessMap = roughnessMap;
p5.prototype.ambientOcclusionMap = ambientOcclusionMap;
p5.prototype.metalnessMap = metalnessMap;
p5.prototype.displacementMap = displacementMap;
p5.prototype.bumpMap = bumpMap;
p5.prototype.envMap = envMap;

p5.prototype.noNormalMap = noNormalMap;
p5.prototype.noDiffuseMap = noDiffuseMap;
p5.prototype.noRoughnessMap = noRoughnessMap;
p5.prototype.noMetalnessMap = noMetalnessMap;
p5.prototype.noDisplacementMap = noDisplacementMap;
p5.prototype.noBumpMap = noBumpMap;
p5.prototype.noEnvMap = noEnvMap;

p5.prototype.pointLight = pointLight; //Overwriten
p5.prototype.ambientLight = ambientLight; //Overwriten

p5.prototype.environment = environment;
p5.prototype.noEnvironment = noEnvironment;
p5.prototype.getEnvironment = getEnvironment;


p5.prototype.getPresets = getPresets;
p5.prototype.exposure = exposure;


p5.prototype.align = align;
p5.prototype.FORWARD = FORWARD;
p5.prototype.BACKWARD = BACKWARD;

p5.prototype.shape = shape;
p5.prototype.image3D = image3D;

p5.prototype.loadFont3D = loadFont3D;
p5.prototype.box = box; //Overwriten
p5.prototype.cube = cube;
p5.prototype.plane = plane; //Overwriten
//p5.prototype.circle = circle;
p5.prototype.ring = ring;
p5.prototype.sphere = sphere; //Overwriten
p5.prototype.ellipsoid = ellipsoid; //Overwriten
p5.prototype.cone = cone; //Overwriten
p5.prototype.capsule = capsule;
p5.prototype.cylinder = cylinder; //Overwriten
p5.prototype.torus = torus; //Overwriten
p5.prototype.icosahedron = icosahedron;
p5.prototype.octahedron = octahedron;
p5.prototype.tetrahedron = tetrahedron;
p5.prototype.pyramid = pyramid;

p5.prototype.smoothShading = smoothShading;
p5.prototype.flatShading = flatShading;
p5.prototype.quality = quality;
p5.prototype.setQuality = setQuality;

p5.prototype.FULL_HD = FULL_HD;
p5.prototype.HD = HD;
p5.prototype.SD = SD;
p5.prototype.LD = LD;
//p5.prototype.color = color;
p5.prototype.randomColor = randomColor;

// p5.prototype.hue = hue;
// p5.prototype.saturation = saturation;
// p5.prototype.lightness = lightness;

p5.prototype.background3D = background3D;
p5.prototype.diffuse = diffuse;
p5.prototype.getDiffuse = getDiffuse;
p5.prototype.inspect = inspect;

p5.prototype.subtract = subtract;
p5.prototype.union = union;
p5.prototype.intersect = intersect;

p5.prototype.material = material;
p5.prototype.emissive = emissive;
p5.prototype.emissiveIntensity = emissiveIntensity;
p5.prototype.roughness = roughness;
p5.prototype.opacity = opacity;
p5.prototype.metalness = metalness;

p5.prototype.getEmissiveIntensity = getEmissiveIntensity;
p5.prototype.getEmissive = getEmissive;
p5.prototype.getRoughness = getRoughness;
p5.prototype.getOpacity = getOpacity;
p5.prototype.getMetalness = getMetalness;

p5.prototype.Mesh3D = Mesh3D;
p5.prototype.Group3D = Group3D;
//p5.prototype.Color = Color;
p5.prototype.VertexNormalsHelper = VertexNormalsHelper;

p5.prototype.object3D = null;

p5.prototype.createCanvas3D = createCanvas3D;

p5.prototype.render3D = render3D;

p5.prototype.lookAt = lookAt;
p5.prototype.setCamera = setCamera;
p5.prototype.changeCamera = changeCamera;
p5.prototype.pickObject = pickObject;

p5.prototype.pushFX = pushFX;
p5.prototype.popFX = popFX;
p5.prototype.getFX = getFX;
p5.prototype.removeFX = removeFX;
p5.prototype.clearFX = clearFX;
p5.prototype.anaglyph = anaglyph;

p5.prototype.GLITCH = GLITCH;
p5.prototype.LUMINOSITY = LUMINOSITY;
p5.prototype.SOBEL = SOBEL;
p5.prototype.BLOOM = BLOOM;
p5.prototype.PIXEL = PIXEL;
p5.prototype.HALFTONE = HALFTONE;
//p5.prototype.DEPTH = DEPTH;


p5.prototype.HALFTONE_DOT = HALFTONE_DOT;
p5.prototype.HALFTONE_ELLIPSE = HALFTONE_ELLIPSE;
p5.prototype.HALFTONE_LINE = HALFTONE_LINE;
p5.prototype.HALFTONE_SQUARE = HALFTONE_SQUARE;


p5.prototype.ORTHOGRAPHIC = ORTHOGRAPHIC;
p5.prototype.PERSPECTIVE = PERSPECTIVE;
p5.prototype.CINEMA = CINEMA;
p5.prototype.ROTUNDA = ROTUNDA;
p5.prototype.SUNSET = SUNSET;
p5.prototype.SWITZERLAND = SWITZERLAND;
p5.prototype.NIGHT = NIGHT;
p5.prototype.NEON = NEON;
p5.prototype.VERANDA = VERANDA;
p5.prototype.STARS = STARS;
p5.prototype.FOREST = FOREST;
p5.prototype.UBUNTU = UBUNTU;

p5.prototype.loadSheet = loadSheet;
p5.prototype.loadAsset = loadAsset;
p5.prototype.loadObject = loadObject;
p5.prototype.subdivide = subdivide;


p5.prototype.skybox = skybox;
p5.prototype.noSkybox = noSkybox;

p5.prototype.FirstPersonControls = FirstPersonControls;

p5.prototype.registerMethod("init", init3D);
p5.prototype.registerMethod("pre", render3D);

p5.prototype.registerPreloadMethod('loadFont3D', p5.prototype);
p5.prototype.registerPreloadMethod("loadAsset", p5.prototype);
p5.prototype.registerPreloadMethod("loadObject", p5.prototype);
p5.prototype.registerPreloadMethod("loadSheet", p5.prototype);