"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GCodeRenderer = void 0;
const three_1 = require("three");
const OrbitControls_1 = require("three/examples/jsm/controls/OrbitControls");
const parser_1 = require("./parser");
/**
 * GCode renderer which parses a GCode file and displays it using
 * three.js. Use .element() to retrieve the DOM canvas element.
 */
class GCodeRenderer {
    /**
     * Creates a new GCode renderer for the given gcode.
     * It initializes the canvas to the given size and
     * uses the passed color as background.
     *
     * @param {string} gCode
     * @param {number} width
     * @param {number} height
     * @param {Color} background
     */
    constructor(gCode, width, height, background) {
        this.lineMaterial = new three_1.MeshPhongMaterial({ vertexColors: true });
        // Public configurations: 
        /**
         * Here you can replace the default scene setup (called after adding the model).
         * You can use renderer.scene to get access to it and do whatever you want with it.
         * The default implementation just adds some lights and then calls renderer.fitCamera().
         */
        this.setupScene = () => {
            // Set up some lights.
            const ambientLight = new three_1.AmbientLight(0xffffff, 0.5);
            this.scene.add(ambientLight);
            const spotLight = new three_1.SpotLight(0xffffff, 0.9);
            spotLight.position.set(200, 400, 300);
            spotLight.lookAt(new three_1.Vector3(0, 0, 0));
            const spotLight2 = new three_1.SpotLight(0xffffff, 0.9);
            spotLight2.position.set(-200, -400, -300);
            spotLight2.lookAt(new three_1.Vector3(0, 0, 0));
            this.scene.add(spotLight);
            this.scene.add(spotLight2);
            this.fitCamera();
        };
        this.scene = new three_1.Scene();
        this.renderer = new three_1.WebGLRenderer();
        this.renderer.setSize(width, height);
        this.renderer.setClearColor(background, 1);
        this.camera = this.newCamera(width, height);
        this.parser = new parser_1.GCodeParser(gCode);
    }
    /**
     * Width of travel-lines. Use 0 to hide them.
     *
     * @type number
     */
    get travelWidth() {
        return this.parser.travelWidth;
    }
    /**
     * Width of travel-lines. Use 0 to hide them.
     *
     * @type number
     */
    set travelWidth(w) {
        this.parser.travelWidth = w;
    }
    /**
     * Set any colorizer implementation to change the segment color based on the segment
     * metadata. Some default implementations are provided.
     *
     * @type SegmentColorizer
     */
    get colorizer() {
        return this.parser.colorizer;
    }
    /**
     * Set any colorizer implementation to change the segment color based on the segment
     * metadata. Some default implementations are provided.
     *
     * @type SegmentColorizer
     */
    set colorizer(colorizer) {
        this.parser.colorizer = colorizer;
    }
    /**
     * The number of radial segments per line.
     * Less (e.g. 3) provides faster rendering with less memory usage.
     * More (e.g. 8) provides a better look.
     *
     * @default 8
     * @type number
     */
    get radialSegments() {
        return this.parser.radialSegments;
    }
    /**
     * The number of radial segments per line.
     * Less (e.g. 3) provides faster rendering with less memory usage.
     * More (e.g. 8) provides a better look.
     *
     * @default 8
     * @type number
     */
    set radialSegments(segments) {
        this.parser.radialSegments = segments;
    }
    /**
     * Internally the rendered object is split into several. This allows to reduce the
     * memory consumption while rendering.
     * You can set the number of points per object.
     * In most cases you can leave this at the default.
     *
     * @default 120000
     * @type number
     */
    get pointsPerObject() {
        return this.parser.pointsPerObject;
    }
    /**
     * Internally the rendered object is split into several. This allows to reduce the
     * memory consumption while rendering.
     * You can set the number of points per object.
     * In most cases you can leave this at the default.
     *
     * @default 120000
     * @type number
     */
    set pointsPerObject(num) {
        this.parser.pointsPerObject = num;
    }
    /**
     * This can be used to retrieve some min / max values which may
     * be needed as param for a colorizer.
     * @returns {{
     *         minTemp: number | undefined,
     *         maxTemp: number,
     *         minSpeed: number | undefined,
     *         maxSpeed: number
     *     }}
     */
    getMinMaxValues() {
        return this.parser.getMinMaxValues();
    }
    newCamera(width, height) {
        var _a;
        const camera = new three_1.PerspectiveCamera(75, width / height, 0.1, 1000);
        if (this.cameraControl) {
            this.cameraControl.dispose();
        }
        this.cameraControl = new OrbitControls_1.OrbitControls(camera, this.renderer.domElement);
        this.cameraControl.enablePan = true;
        this.cameraControl.enableZoom = true;
        this.cameraControl.minDistance = -Infinity;
        this.cameraControl.maxDistance = Infinity;
        (_a = this.cameraControl) === null || _a === void 0 ? void 0 : _a.addEventListener("change", () => {
            requestAnimationFrame(() => this.draw());
        });
        return camera;
    }
    fitCamera() {
        var _a, _b, _c, _d;
        const boundingBox = new three_1.Box3(this.parser.min, this.parser.max);
        const center = new three_1.Vector3();
        boundingBox.getCenter(center);
        this.camera.position.x = (((_a = this.parser.min) === null || _a === void 0 ? void 0 : _a.x) || 0) + ((((_b = this.parser.max) === null || _b === void 0 ? void 0 : _b.x) || 0) - (((_c = this.parser.min) === null || _c === void 0 ? void 0 : _c.x) || 0)) / 2;
        this.camera.position.z = (((_d = this.parser.max) === null || _d === void 0 ? void 0 : _d.z) || 0);
        if (this.cameraControl) {
            this.cameraControl.target = center;
        }
        this.camera.lookAt(center);
        this.draw();
    }
    /**
     * Reads the GCode and renders it to a mesh.
     */
    render() {
        return __awaiter(this, void 0, void 0, function* () {
            this.parser.parse();
            this.parser.getGeometries().forEach(g => {
                this.scene.add(new three_1.Mesh(g, this.lineMaterial));
            });
            this.setupScene();
        });
    }
    draw() {
        if (this.parser.getGeometries().length === 0 || this.camera === undefined) {
            return;
        }
        this.renderer.render(this.scene, this.camera);
    }
    /**
     * Slices the rendered model based on the passed start and end point numbers.
     * (0, pointsCount()) renders everything
     *
     * Note: Currently negative values are not allowed.
     *
     * @param {number} start the starting segment
     * @param {number} end the ending segment (excluding)
     */
    slice(start = 0, end = this.pointsCount()) {
        this.parser.slice(start, end);
        this.draw();
    }
    /**
     * Slices the rendered model based on the passed start and end line numbers.
     * (0, layerCount()) renders everything
     *
     * Note: Currently negative values are not allowed.
     *
     * @param {number} start the starting layer
     * @param {number} end the ending layer (excluding)
     */
    sliceLayer(start, end) {
        this.parser.sliceLayer(start, end);
        this.draw();
    }
    /**
     * Retrieve the dom html canvas element where
     * the GCode viewer draws to.
     * @returns {HTMLCanvasElement}
     */
    element() {
        return this.renderer.domElement;
    }
    /**
     * disposes everything which is dispose able.
     * Call this always before destroying the instance.""
     */
    dispose() {
        var _a;
        (_a = this.cameraControl) === null || _a === void 0 ? void 0 : _a.dispose();
        this.parser.dispose();
    }
    /**
     * Get the amount of points in the model.
     *
     * @returns {number}
     */
    pointsCount() {
        return this.parser.pointsCount();
    }
    /**
     * Get the amount of layers in the model.
     * This is an approximation which may be incorrect if the
     * nozzle moves downwards mid print.
     *
     * @returns {number}
     */
    layerCount() {
        return this.parser.layerCount();
    }
    /**
     * This can be called when a resize of the canvas is needed.
     *
     * @param {number} width
     * @param {number} height
     */
    resize(width, height) {
        this.renderer.setSize(width, height);
        const rot = this.camera.rotation;
        const pos = this.camera.position;
        this.camera = this.newCamera(width, height);
        this.fitCamera();
        this.camera.rotation.copy(rot);
        this.camera.position.copy(pos);
        this.draw();
    }
}
exports.GCodeRenderer = GCodeRenderer;
