let exporterAction; function exportToDeerEngine() { // Build your JSON structure and trigger download var json = { mesh: { hasNormalData: true, vertexPositions: [], vertexNormals: [], indices: [], }, }; function addVertex(vector) { json.mesh.vertexPositions.push({ x: Math.round((vector[0] / 16) * 256), y: Math.round((vector[1] / 16) * 256), z: Math.round((vector[2] / 16) * 256), }); return json.mesh.vertexPositions.length - 1; } function addIndex(indexID) { json.mesh.indices.push(indexID); } function addNormal(normal) { json.mesh.vertexNormals.push({ x: Math.round(normal[0] * 64), y: Math.round(normal[1] * 64), z: Math.round(normal[2] * 64), }); } Mesh.all.forEach((mesh) => { function getWorldPosition(v) { const mat = mesh.mesh.matrixWorld; // mat4 (THREE.Matrix4) const vec = new THREE.Vector3(v[0], v[1], v[2]); vec.applyMatrix4(mat); // Convert to your engine’s left-handed space: flip Y and Z return [vec.x, vec.y, -vec.z]; } for (const fkey in mesh.faces) { const face = mesh.faces[fkey]; var verticesIDs = []; for (const vKey of face.vertices) { const position = getWorldPosition(mesh.vertices[vKey]); verticesIDs.push(addVertex(position)); } // DEER ENGINE IS CLOCK FACE //for (var i = 0; i < verticesIDs.length; i++) { // addIndex(verticesIDs[i]); //} if (verticesIDs.length == 3) { addIndex(verticesIDs[0]); addIndex(verticesIDs[2]); addIndex(verticesIDs[1]); } else if (verticesIDs.length == 4) { addIndex(verticesIDs[0]); addIndex(verticesIDs[2]); addIndex(verticesIDs[1]); addIndex(verticesIDs[1]); addIndex(verticesIDs[2]); addIndex(verticesIDs[3]); } // Normal calulation var origin = getWorldPosition(mesh.vertices[face.vertices[0]]); var dirBRef = getWorldPosition(mesh.vertices[face.vertices[1]]); var dirARef = getWorldPosition(mesh.vertices[face.vertices[2]]); var dirA = [0, 0, 0]; var dirB = [0, 0, 0]; for (var i = 0; i < 3; i++) { dirB[i] = dirARef[i] - origin[i]; dirA[i] = dirBRef[i] - origin[i]; } var normalDir = [0, 0, 0]; normalDir[0] = dirB[1] * dirA[2] - dirB[2] * dirA[1]; normalDir[1] = dirB[2] * dirA[0] - dirB[0] * dirA[2]; normalDir[2] = dirB[0] * dirA[1] - dirB[1] * dirA[0]; var normalMagnitude = Math.sqrt( normalDir[0] * normalDir[0] + normalDir[1] * normalDir[1] + normalDir[2] * normalDir[2] ); for (var i = 0; i < 3; i++) { normalDir[i] = normalDir[i] / normalMagnitude; } for (var i = 0; i < verticesIDs.length; i++) { addNormal(normalDir); } } }); const face_corners = { north: [5, 4, 7, 6], east: [1, 5, 3, 7], south: [0, 1, 2, 3], west: [4, 0, 6, 2], up: [2, 3, 6, 7], down: [4, 5, 0, 1], }; Cube.all.forEach((cube) => { // Cubes are simpler: they have 6 faces and 8 vertices const mat = cube.mesh.matrixWorld; function getWorldPosition(v) { const vec = new THREE.Vector3(v[0], v[1], v[2]); vec.applyMatrix4(mat); return [vec.x, vec.y, -vec.z]; // Flip Z to match left-handed } // cube.from and cube.to define the min and max corners const from = cube.from; // [x, y, z] const to = cube.to; // [x, y, z] const corners = [ [from[0], from[1], from[2]], [to[0], from[1], from[2]], [from[0], to[1], from[2]], [to[0], to[1], from[2]], [from[0], from[1], to[2]], [to[0], from[1], to[2]], [from[0], to[1], to[2]], [to[0], to[1], to[2]], ]; for (var i = 0; i < corners.length; i++) { corners[i][0] -= cube.origin[0]; corners[i][1] -= cube.origin[1]; corners[i][2] -= cube.origin[2]; } for (const fkey in cube.faces) { const face = cube.faces[fkey]; const verticesIDs = []; const corner_indices = face_corners[fkey]; // get the right 4 corner indices if (!corner_indices) continue; // just in case const face_vertices = corner_indices.map((i) => corners[i]); for (const v of face_vertices) { const position = getWorldPosition(v); verticesIDs.push(addVertex(position)); } if (verticesIDs.length == 4) { addIndex(verticesIDs[0]); addIndex(verticesIDs[1]); addIndex(verticesIDs[2]); addIndex(verticesIDs[1]); addIndex(verticesIDs[3]); addIndex(verticesIDs[2]); } // Calculate normal like in your mesh code var origin = getWorldPosition(face_vertices[0]); var dirBRef = getWorldPosition(face_vertices[1]); var dirARef = getWorldPosition(face_vertices[2]); var dirA = [0, 0, 0]; var dirB = [0, 0, 0]; for (var i = 0; i < 3; i++) { dirA[i] = dirARef[i] - origin[i]; dirB[i] = dirBRef[i] - origin[i]; } var normalDir = [0, 0, 0]; normalDir[0] = dirB[1] * dirA[2] - dirB[2] * dirA[1]; normalDir[1] = dirB[2] * dirA[0] - dirB[0] * dirA[2]; normalDir[2] = dirB[0] * dirA[1] - dirB[1] * dirA[0]; var normalMagnitude = Math.sqrt( normalDir[0] * normalDir[0] + normalDir[1] * normalDir[1] + normalDir[2] * normalDir[2] ); for (var i = 0; i < 3; i++) { normalDir[i] = normalDir[i] / normalMagnitude; } for (var i = 0; i < verticesIDs.length; i++) { addNormal(normalDir); } } }); Blockbench.export({ type: "DeerMesh", extensions: ["dmesh"], name: Project.name, content: autoStringify(json), }); /* Blockbench.showQuickMessage( PathModule.join(folder, Project.name + ".dmesh"), 10000 ); Blockbench.writeFile(PathModule.join(folder, Project.name + ".dmesh"), { content: autoStringify(json), });*/ } Plugin.register("deer_engine_exporter", { title: "Deer Engine", author: "Chewico", description: "Utilities to work with Deer Engine", icon: "export", // any valid Blockbench icon :contentReference[oaicite:5]{index=5} version: "1.0.0", variant: "both", // supports desktop & web onload() { // (1) Create the export action exporterAction = new Action("export_deer_engine_json", { name: "Export to Deer Engine Mesh Format", icon: "export", click() { //visibleOverridesDialog.show(); exportToDeerEngine(); }, }); // (2) Add it under File → Export (top position) MenuBar.addAction(exporterAction, "file.export.0"); // “export” submenu, index 0 :contentReference[oaicite:6]{index=6} }, onunload() { // Remove the action when plugin unloads exporterAction.delete(); }, });