<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.canvas.Canvas?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.input.KeyCodeCombination?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Text?>

<?import javafx.scene.control.TextField?>
<AnchorPane fx:id="anchorPane" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.cgvsu.GuiController">


    <MenuBar>
        <Menu mnemonicParsing="false" text="File">
            <MenuItem mnemonicParsing="false" onAction="#onOpenModelMenuItemClick" text="Load Model">
                <accelerator>
                    <KeyCodeCombination alt="UP" code="F" control="DOWN" meta="UP" shift="UP" shortcut="UP" />
                </accelerator>
            </MenuItem>
        </Menu>
        <Menu mnemonicParsing="false" text="Camera Options">
            <MenuItem mnemonicParsing="false" onAction="#handleCameraForward" text="Forward">
                <accelerator>
                    <KeyCodeCombination alt="UP" code="UP" control="UP" meta="UP" shift="UP" shortcut="UP" />
                </accelerator>
            </MenuItem>
            <MenuItem mnemonicParsing="false" onAction="#handleCameraBackward" text="Backward">
                <accelerator>
                    <KeyCodeCombination alt="UP" code="DOWN" control="UP" meta="UP" shift="UP" shortcut="UP" />
                </accelerator>
            </MenuItem>
            <MenuItem mnemonicParsing="false" onAction="#handleCameraLeft" text="Left">
                <accelerator>
                    <KeyCodeCombination alt="UP" code="LEFT" control="UP" meta="UP" shift="UP" shortcut="UP" />
                </accelerator>
            </MenuItem>
            <MenuItem mnemonicParsing="false" onAction="#handleCameraRight" text="Right">
                <accelerator>
                    <KeyCodeCombination alt="UP" code="RIGHT" control="UP" meta="UP" shift="UP" shortcut="UP" />
                </accelerator>
            </MenuItem>
            <MenuItem mnemonicParsing="false" onAction="#handleCameraUp" text="Up">
                <accelerator>
                    <KeyCodeCombination alt="UP" code="W" control="UP" meta="UP" shift="UP" shortcut="UP" />
                </accelerator>
            </MenuItem>
            <MenuItem mnemonicParsing="false" onAction="#handleCameraDown" text="Down">
                <accelerator>
                    <KeyCodeCombination alt="UP" code="S" control="UP" meta="UP" shift="UP" shortcut="UP" />
                </accelerator>
            </MenuItem>
        </Menu>
        <Menu mnemonicParsing="false" text="Change the model">
            <MenuItem mnemonicParsing="false" onAction="#affineTransformations" text="Change">

            </MenuItem>

        </Menu>
    </MenuBar>

    <Canvas fx:id="canvas" height="870" width="976.0" AnchorPane.topAnchor="30.0" />
</AnchorPane>
package com.cgvsu;

import com.cgvsu.math.Vector3f;
import com.cgvsu.model.Model;
import com.cgvsu.objreader.ObjReader;
import com.cgvsu.render_engine.Camera;
import com.cgvsu.render_engine.RenderEngine;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class GuiController {

    final private float TRANSLATION = 0.5F;

    @FXML
    AnchorPane anchorPane;

    @FXML
    private Canvas canvas;

    private Model mesh = null;

    private Camera camera = new Camera(
            new Vector3f(0, 0, 100),
            new Vector3f(0, 0, 0),
            1.0F, 1, 0.01F, 100);

    private Timeline timeline;

    @FXML
    private void initialize() {
        anchorPane.prefWidthProperty().addListener((ov, oldValue, newValue) -> canvas.setWidth(newValue.doubleValue()));
        anchorPane.prefHeightProperty().addListener((ov, oldValue, newValue) -> canvas.setHeight(newValue.doubleValue()));

        timeline = new Timeline();
        timeline.setCycleCount(Animation.INDEFINITE);

        KeyFrame frame = new KeyFrame(Duration.millis(15), event -> {
            double width = canvas.getWidth();
            double height = canvas.getHeight();

            canvas.getGraphicsContext2D().clearRect(0, 0, width, height);
            camera.setAspectRatio((float) (width / height));

            if (mesh != null) {
                RenderEngine.render(canvas.getGraphicsContext2D(), camera, mesh, (int) width, (int) height);
            }
        });

        timeline.getKeyFrames().add(frame);
        timeline.play();
    }

    @FXML
    private void onOpenModelMenuItemClick() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Model (*.obj)", "*.obj"));
        fileChooser.setTitle("Load Model");

        File initialDirectory = new File("3DModels");
        if (initialDirectory.exists()) {
            fileChooser.setInitialDirectory(initialDirectory);
        }

        File file = fileChooser.showOpenDialog((Stage) canvas.getScene().getWindow());
        if (file == null) {
            return;
        }

        Path fileName = Path.of(file.getAbsolutePath());

        try {
            String fileContent = Files.readString(fileName);
            mesh = ObjReader.read(fileContent);
            mesh.triangulate();


            // todo: обработка ошибок
        } catch (IOException exception) {

        }
    }

    @FXML
    public void handleCameraForward(ActionEvent actionEvent) {
        Vector3f currentPosition = camera.getPosition();
        Vector3f newPosition = currentPosition.add(new Vector3f(0, 0, -TRANSLATION));
        camera.setPosition(newPosition);
    }

    @FXML
    public void handleCameraBackward(ActionEvent actionEvent) {
        Vector3f currentPosition = camera.getPosition();
        Vector3f newPosition = currentPosition.add(new Vector3f(0, 0, TRANSLATION));
        camera.setPosition(newPosition);
    }

    @FXML
    public void handleCameraLeft(ActionEvent actionEvent) {
        Vector3f currentPosition = camera.getPosition();
        Vector3f newPosition = currentPosition.add(new Vector3f(TRANSLATION, 0, 0));
        camera.setPosition(newPosition);
    }

    @FXML
    public void handleCameraRight(ActionEvent actionEvent) {
        Vector3f currentPosition = camera.getPosition();
        Vector3f newPosition = currentPosition.add(new Vector3f(-TRANSLATION, 0, 0));
        camera.setPosition(newPosition);
    }

    @FXML
    public void handleCameraUp(ActionEvent actionEvent) {
        Vector3f currentPosition = camera.getPosition();
        Vector3f newPosition = currentPosition.add(new Vector3f(0, TRANSLATION, 0));
        camera.setPosition(newPosition);
    }

    @FXML
    public void handleCameraDown(ActionEvent actionEvent) {
        Vector3f currentPosition = camera.getPosition();
        Vector3f newPosition = currentPosition.add(new Vector3f(0, -TRANSLATION, 0));
        camera.setPosition(newPosition);
    }

    @FXML
    public void affineTransformations() {


    }
}
я хочу добавить несколько доп объектов в мое окно: функию перерасчета нормалей. Для этого надо в окне сделать 9 полей, куда пользователь будет вводить углы разворота, координаты переноса и масштаб. обрабаатывать должна функция - affineTransformations(). которая в свою очередь должна перерасчитывать нормали:
package com.cgvsu.render_engine;

import com.cgvsu.math.*;

import static java.lang.Math.*;


public class GraphicConveyor {

    /**
     * Аффинные преобразования
     * она кстати уникальная для каждого объекта... пупупу
     *
     * @return {@code Matrix4f} - матрица из локальной системы координат в мировую
     */
    public static Matrix4f rotateScaleTranslate(Vector3f rotate, Vector3f scale, Vector3f translate) {
        Matrix4f r = calculateRotate(rotate.getX(),rotate.getY(), rotate.getZ());
        Matrix4f s = calculateScale(scale.getX(),scale.getY(), scale.getZ());
        Matrix4f t = calculateTranslate(new Vector3f(translate.getX(),translate.getY(), translate.getZ()));

        t.multiply(r);
        t.multiply(s);

        return new Matrix4f(t);
    }

    /**
     * Аффинные преобразования - увеличение/уменьшение объекта
     *
     * @param vector3f вектор переноса
     * @return {@code Matrix4f} - матрица переноса
     */

    public static Matrix4f calculateTranslate(Vector3f vector3f) {
        return calculateTranslate(new Vector4f(vector3f.getX(),vector3f.getY(),vector3f.getZ(),1));
    }

    private static  Matrix4f calculateTranslate(Vector4f vector) {
        Matrix4f res = new Matrix4f(1);
        for (int i = 0; i < res.getElements().length-1; i++) {
            res.setElement(i, 3, vector.getNum(i));
        }
        return res;
    }


    public static Matrix4f Rotate(float alpha, float beta, float gamma) {
        return calculateRotate(alpha, beta, gamma);
    }

    // мб потом почитаю что такое кватернион и немного переделаю код, пока пусть так будет
    private static Matrix4f calculateRotate(float alpha, float beta, float gamma) {
        // вокруг x
        float[] rotateX = new float[]{
                1, 0, 0, 0,
                0, (float) cos(alpha), (float) sin(alpha), 0,
                0, (float) -sin(alpha), (float) cos(alpha), 0,
                0, 0, 0, 1
        };
        // вокруг y
        float[] rotateY = new float[]{
                (float) cos(beta), 0, (float) sin(beta), 0,
                0, 1, 0, 0,
                (float) -sin(beta), 0, (float) cos(beta), 0,
                0, 0, 0, 1
        };
        // вокруг z
        float[] rotateZ = new float[]{
                (float) cos(gamma), (float) sin(gamma), 0, 0,
                (float) -sin(gamma), (float) cos(gamma), 0, 0,
                0, 0, 1, 0,
                0, 0, 0, 1
        };

        Matrix4f rotateAboutX = new Matrix4f(rotateX);
        Matrix4f rotateAboutY = new Matrix4f(rotateY);
        Matrix4f rotateAboutZ = new Matrix4f(rotateZ);

        // сначала крутим вокруг x, потом y, потом z (да, в реале умножаем сначала Z, потом ыгрик, потом х (а) - плохо)
        rotateAboutZ.multiply(rotateAboutY);
        rotateAboutZ.multiply(rotateAboutX);

        return rotateAboutZ;
    }

    /**
     * Аффинные преобразования - увеличение/уменьшение объекта
     *
     * @param scaleX растяжение по x
     * @param scaleY растяжение по y
     * @param scaleZ растяжение по z
     * @return {@code Matrix4f} - матрица растяжения (по факту просто по главной диагонали будут стоять не 1, а растяжение наше)
     */

    public static Matrix4f calculateScale(float scaleX, float scaleY, float scaleZ) {
        float[] scale = new float[]{scaleX, scaleY, scaleZ};
        return calculateScale(scale);
    }

    // надо еще продумать, как обновлять это все... наверное, потом придумаю... надеюсь, молюсь...

    private static Matrix4f calculateScale(float[] scale) {
        Matrix4f result = new Matrix4f(1);
        for (int i = 0; i < scale.length; i++) {
            result.setElement(i, i, scale[i]);
        }
        return result;
    }

    public static Matrix4f lookAt(Vector3f eye, Vector3f target) {
        return lookAt(eye, target, new Vector3f(0F, 1.0F, 0F));
    }

    /**
     * Система корд камеры +перевод всех объектов сцены в систему координат камеры
     *
     * @param eye    ее позиция в мировых координатах
     * @param target куда смотрит наша камера
     * @param up     единичный вектор для y - основа будущих корд камеры
     * @return {@code Matrix4f} - матрица прямоугольной системы координат камеры, записанная в мировых координатах сцены.
     */

    private static Matrix4f lookAt(Vector3f eye, Vector3f target, Vector3f up) {
        Vector3f resultX = new Vector3f();
        Vector3f resultY = new Vector3f();
        Vector3f resultZ = new Vector3f();

        resultZ.sub(target, eye);
        resultX = up.cross(resultZ);
        resultY = resultZ.cross(resultX);

        resultX.normalize();
        resultY.normalize();
        resultZ.normalize();

        float[] matrix = new float[]{
                resultX.getX(), resultY.getX(), resultZ.getX(), 0,
                resultX.getY(), resultY.getY(), resultZ.getY(), 0,
                resultX.getZ(), resultY.getZ(), resultZ.getZ(), 0,
                -resultX.dot(eye), -resultY.dot(eye), -resultZ.dot(eye), 1};
        return new Matrix4f(matrix);
    }

    /**
     * Матрица перспективы.
     *
     * @param fov         угол обзора
     * @param aspectRatio соотношение сторон экрана
     * @param nearPlane   ближний план
     * @param farPlane    дальний план
     * @return {@code Matrix4f} - матрица перспективы.
     */

    public static Matrix4f perspective(
            final float fov,
            final float aspectRatio,
            final float nearPlane,
            final float farPlane) {
        float tangentMinusOnDegree = (float) (1.0F / (tan(fov * 0.5F))); // fov задается как половина от всего угла
        float[] res = {
                tangentMinusOnDegree / aspectRatio, 0, 0, 0,
                0, tangentMinusOnDegree, 0, 0,
                0, 0, (farPlane + nearPlane) / (farPlane - nearPlane), 1.0F,
                0, 0, 2 * (nearPlane * farPlane) / (nearPlane - farPlane), 0
        };
        return new Matrix4f(res);
    }

    /**
     * Получаем последний вектор нормализованный вектор v для трансформации вершины и отображения последней на экране
     *
     * @param matrix P*V*M
     * @param vertex корды точки?
     * @return {@code Vector3f} - нормализованный конечный вектор (скорее всего, нормаль к вершине).
     */
    public static Vector3f multiplyMatrix4ByVector3(final Matrix4f matrix, final Vector3f vertex) {
        Vector4f res = matrix.multiply(new Vector4f(vertex.getX(), vertex.getY(), vertex.getZ(), 1));
        float w = res.getW();
        return new Vector3f(res.getX() / w, res.getY() / w, res.getZ() / w);
    }

    /**
     * Получаем последний вектор нормализованный вектор v для трансформации вершины и отображения последней на экране
     *
     * @param width  ширина
     * @param height высота
     * @param vertex нормаль вершины
     * @return {@code Point2f} - получаем корды точки на экране.
     */

    public static Point2f vertexToPoint(final Vector3f vertex, final int width, final int height) {
        return new Point2f(vertex.getX() * width + width / 2.0F, -vertex.getY() * height + height / 2.0F);
    }
}