@@ -130,13 +130,13 @@ For applying modifications on selected vertices, edges or faces.
hub Bridge Edge Loops
NEW
+ border: max(1px, 0.0625rem) solid var(--color-accent);
+ color: var(--color-accent);
+ border-radius: 2em;
+ font-size: .75rem;
+ font-weight: 500;
+ padding: 0 7px;
+ white-space: nowrap;">NEW
Access From:
@@ -147,39 +147,37 @@ Access From:
Connects multiple edge loops with faces.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
Results with Blend Path enabled.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
@@ -198,38 +196,36 @@ Access From:
Casts selected vertices into a smooth, spherical shape with adjustable influence.
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
-
+
+
@@ -248,22 +244,21 @@ Access From:
Smoothens selected vertices by averaging the position of neighboring vertices.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
@@ -282,22 +277,21 @@ Access From:
Generates a fan out of a face.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
@@ -316,22 +310,21 @@ Access From:
Attempts to merge adjacent triangles into quadrilaterals.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
@@ -350,22 +343,21 @@ Access From:
Splits selected faces into triangles.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
@@ -387,22 +379,21 @@ Access From:
Projects the selected faces to the UV map from the camera.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
@@ -424,22 +415,21 @@ Access From:
Unwraps the UV map from the 6 sides of a cube.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
@@ -499,22 +489,21 @@ Access From:
Splits the faces of a mesh into smaller faces, giving it a smooth appearance.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
@@ -533,16 +522,15 @@ Access From:
Splits and duplicates edges within a mesh, breaking 'links' between faces around those split edges.
-
-
-
-
-
+
+
+
-
+
+
@@ -561,16 +549,15 @@ Access From:
Scatters selected meshes on the active mesh.
-
-
-
-
-
+
+
+
-
+
+
@@ -589,16 +576,15 @@ Access From:
Generates an array of copies of the base object, with each copy being offset from the previous one.
-
-
-
-
-
+
+
+
-
+
+
@@ -620,22 +606,21 @@ Access From:
Generates terrains procedurally with fully customized settings.
-
-
-
-
-
+
+
+
+
-
-
+
-
+
+
@@ -673,16 +658,15 @@ Access From:
Converts text into a 3D object, ideal for creating signs or logos.
-
-
-
-
-
+
+
+
-
+
+
@@ -701,16 +685,15 @@ Access From:
Generates an xyz surface based on mathematical equations containing 23 pre-built presets!
-
-
-
-
-
+
+
+
-
+
+
@@ -732,16 +715,15 @@ Access From:
Generate a polyhedron such as an Icosahedron, a Dodecahedron, an Octahedron or a Tetrahedron.
-
-
-
-
-
+
+
+
-
+
+
@@ -763,16 +745,15 @@ Access From:
Generate a Torus Knot with fully customized settings.
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/plugins/mesh_tools/mesh_tools.js b/plugins/mesh_tools/mesh_tools.js
index e68ec88a..a79298db 100644
--- a/plugins/mesh_tools/mesh_tools.js
+++ b/plugins/mesh_tools/mesh_tools.js
@@ -1,6 +1,17 @@
(function () {
'use strict';
+ var bevel = {
+ docs: {
+ "private": true
+ },
+ name: "Bevel",
+ icon: "rounded_corner",
+ description: "Chamfers selected edges",
+ selection_mode: [
+ "edge"
+ ]
+ };
var laplacian_smooth = {
docs: {
lines: [
@@ -493,6 +504,7 @@
description: "Generate a Torus Knot with fully customized settings."
};
var _ACTIONS = {
+ bevel: bevel,
laplacian_smooth: laplacian_smooth,
to_sphere: to_sphere,
bridge_edge_loops: bridge_edge_loops,
@@ -520,6 +532,119 @@
torusknot: torusknot
};
+ const KEYS_KEY = "
";
+ const SUBS_KEY = "";
+ class BasicQualifiedStorage {
+ constructor(id) {
+ this.id = id;
+ }
+ #isQualified() {
+ return this.id.startsWith("@");
+ }
+ qualifyKey(key) {
+ if (this.#isQualified()) {
+ return `${this.id}/${key}`;
+ }
+ return `@${this.id}/${key}`;
+ }
+ set(key, value) {
+ key = this.qualifyKey(key);
+
+ localStorage.setItem(key, JSON.stringify(value));
+ }
+ delete(key) {
+ key = this.qualifyKey(key);
+
+ localStorage.removeItem(key);
+ }
+ has(key) {
+ key = this.qualifyKey(key);
+
+ return localStorage.hasOwnProperty(key);
+ }
+ get(key) {
+ key = this.qualifyKey(key);
+
+ const rawValue = localStorage.getItem(key);
+ if (rawValue != null) {
+ return JSON.parse(rawValue);
+ }
+ return null;
+ }
+ update(key, callback, defaultValue) {
+ const value = this.get(key) ?? defaultValue;
+ const newValue = callback(value);
+ return this.set(key, newValue);
+ }
+ }
+
+ const keysStorage = new BasicQualifiedStorage(KEYS_KEY);
+ const subStoragesStorage = new BasicQualifiedStorage(SUBS_KEY);
+ class QualifiedStorage extends BasicQualifiedStorage {
+
+ in(key) {
+ subStoragesStorage.update(this.id, (keys) => {
+ keys.safePush(key);
+ return keys;
+ }, []);
+ return new QualifiedStorage(this.qualifyKey(key));
+ }
+
+ constructor(id) {
+ console.assert(
+ id != KEYS_KEY,
+ `QualifiedStorage: id cannot be equal to ${JSON.stringify(KEYS_KEY)}`
+ );
+
+ super(id);
+ }
+ set(key, value) {
+ keysStorage.update(
+ this.id,
+ (keys) => {
+ keys.safePush(key);
+ return keys;
+ },
+ []
+ );
+
+ super.set(key, value);
+ }
+ delete(key) {
+ keysStorage.update(
+ this.id,
+ (keys) => {
+ const index = keys.indexOf(key);
+ if (index != -1) {
+ keys.splice(index, 1);
+ }
+
+ return keys;
+ },
+ []
+ );
+
+ super.delete(key);
+ }
+ getAllKeys() {
+ return keysStorage.get(this.id) ?? [];
+ }
+ clear() {
+ for (const key of this.getAllKeys()) {
+ super.delete(key);
+ }
+ const subKeys = subStoragesStorage.get(this.id) ?? [];
+ for (const subKey of subKeys) {
+ new QualifiedStorage(this.qualifyKey(subKey)).clear();
+ }
+ keysStorage.delete(this.id);
+ subStoragesStorage.delete(this.id);
+ }
+ }
+
+ const PLUGIN_ID = "mesh_tools";
+ const storage = new QualifiedStorage(PLUGIN_ID);
+
const ACTIONS = _ACTIONS;
const CONDITIONS = {
@@ -542,7 +667,7 @@
};
- const qualifyName = (id) => (id == "_" ? id : `@meshtools/${id}`);
+ const qualifyName = (id) => (id == "_" ? id : `@${PLUGIN_ID}/${id}`);
/**
*
@@ -576,6 +701,176 @@
return new Action(qualifyName(id), options);
}
+ /**
+ * @template {V}
+ * @template {K}
+ * @param {V[]} arr
+ * @param {(value: V, currentIndex: number, array: V[]) => K[]} callback
+ * @returns {{[k: K]: V[]}}
+ */
+
+ function minIndex(array) {
+ let minI = -1;
+ let minValue = Infinity;
+ for (let i = 0; i < array.length; i++) {
+ const value = array[i];
+
+ if (value <= minValue) {
+ minValue = value;
+ minI = i;
+ }
+ }
+ return minI;
+ }
+ function findMin(array, map = (x) => x) {
+ if (array.length == 1) return array[0];
+ if (array.length == 0) return null;
+
+ let minElement = null;
+ let minValue = Infinity;
+
+ for (const element of array) {
+ const value = map(element);
+
+ if (value <= minValue) {
+ minElement = element;
+ minValue = value;
+ }
+ }
+
+ return minElement;
+ }
+
+ /**
+ *
+ * @param {ArrayVector3} a
+ * @param {ArrayVector3} b
+ * @param {number} t
+ * @returns {ArrayVector3}
+ */
+ function lerp3(a, b, t) {
+ return a.map((e, i) => Math.lerp(e, b[i], t));
+ }
+ function groupElementsCollided(array, every = 2) {
+ const newArray = [];
+ for (let i = 0; i < array.length; i++) {
+ const sub = [];
+ for (let j = 0; j < every; j++) {
+ const element = array[(i + j) % array.length];
+ sub.push(element);
+ }
+ newArray.push(sub);
+ }
+ return newArray;
+ }
+
+ function offsetArray(array, offset) {
+ while (offset < 0) offset += array.length;
+ while (offset >= array.length) offset -= array.length;
+
+ const newArr = [];
+ for (let i = 0; i < array.length; i++) {
+ newArr[(i + offset) % array.length] = array[i];
+ }
+
+ array.splice(0, Infinity, ...newArr);
+ }
+
+ class Neighborhood {
+ /**
+ *
+ * @param {Mesh} mesh
+ * @returns {{[vertexKey: string]: string[]}}
+ */
+ static VertexVertices(mesh) {
+ const map = {};
+
+ for (const key in mesh.faces) {
+ const face = mesh.faces[key];
+
+ face.vertices.forEach((vkey) => {
+ if (!(vkey in map)) {
+ map[vkey] = [];
+ }
+
+ face.vertices.forEach((neighborkey) => {
+ if (neighborkey == vkey) return;
+
+ map[vkey].safePush(neighborkey);
+ });
+ });
+ }
+
+ return map;
+ }
+
+ /**
+ *
+ * @param {Mesh} mesh
+ * @returns {{[vertexKey: string]: MeshFace[]}}
+ */
+ static VertexFaces(mesh) {
+ const neighborhood = {};
+
+ for (const key in mesh.faces) {
+ const face = mesh.faces[key];
+
+ for (const vertexKey of face.vertices) {
+ neighborhood[vertexKey] ??= [];
+ neighborhood[vertexKey].safePush(face);
+ }
+ }
+
+ return neighborhood;
+ }
+
+ /**
+ *
+ * @param {Mesh} mesh
+ * @returns {{[edgeKey: string]: MeshFace[]}}
+ */
+ static EdgeFaces(mesh) {
+ const neighborhood = {};
+ for (const key in mesh.faces) {
+ const face = mesh.faces[key];
+ const vertices = face.getSortedVertices();
+
+ for (let i = 0; i < vertices.length; i++) {
+ const vertexCurr = vertices[i];
+ const vertexNext = vertices[(i + 1) % vertices.length];
+ const edgeKey = getEdgeKey(vertexCurr, vertexNext);
+ neighborhood[edgeKey] ??= [];
+ neighborhood[edgeKey].safePush(face);
+ }
+ }
+ return neighborhood;
+ }
+
+ /**
+ *
+ * @param {Mesh} mesh
+ * @returns {{[vertexKey: string]: string[]}}
+ */
+ static VertexEdges(mesh) {
+ const neighborhood = {};
+ for (const key in mesh.faces) {
+ const face = mesh.faces[key];
+ const vertices = face.getSortedVertices();
+
+ for (let i = 0; i < vertices.length; i++) {
+ const vertexCurr = vertices[i];
+ const vertexNext = vertices[(i + 1) % vertices.length];
+ const edgeKey = getEdgeKey(vertexCurr, vertexNext);
+ neighborhood[vertexCurr] ??= [];
+ neighborhood[vertexNext] ??= [];
+ neighborhood[vertexCurr].safePush(edgeKey);
+ neighborhood[vertexNext].safePush(edgeKey);
+ }
+ }
+ return neighborhood;
+ }
+ }
+
function xKey(obj) {
if (obj instanceof THREE.Vector3 || obj instanceof THREE.Vector2) {
return "x";
@@ -655,11 +950,11 @@
const reusableEuler1$1 = new THREE.Euler();
const reusableQuat1 = new THREE.Quaternion();
- const reusableVec1 = new THREE.Vector3();
- const reusableVec2 = new THREE.Vector3();
- const reusableVec3 = new THREE.Vector3();
- const reusableVec4 = new THREE.Vector3();
- const reusableVec5 = new THREE.Vector3();
+ const reusableVec1$1 = new THREE.Vector3();
+ const reusableVec2$1 = new THREE.Vector3();
+ const reusableVec3$1 = new THREE.Vector3();
+ const reusableVec4$1 = new THREE.Vector3();
+ const reusableVec5$1 = new THREE.Vector3();
new THREE.Vector2();
new THREE.Vector2(1, 0);
@@ -674,9 +969,16 @@
*/
const reusableObject = new THREE.Object3D();
reusableObject.rotation.order = "XYZ";
- function rotationFromDirection(target, targetEuler = new THREE.Euler()) {
+ function rotationFromDirection(
+ target,
+ targetEuler = new THREE.Euler(),
+ { rotateX = 0, rotateY = 0, rotateZ = 0 } = {}
+ ) {
reusableObject.lookAt(target);
reusableObject.rotateX(Math.degToRad(90));
+ reusableObject.rotateX(rotateX);
+ reusableObject.rotateY(rotateY);
+ reusableObject.rotateZ(rotateZ);
targetEuler.copy(reusableObject.rotation);
return targetEuler;
@@ -689,13 +991,13 @@
* @returns {THREE.Vector3}
*/
function computeTriangleNormal(A, B, C) {
- reusableVec1.set(getX(A), getY(A), getZ(A));
- reusableVec2.set(getX(B), getY(B), getZ(B));
- reusableVec3.set(getX(C), getY(C), getZ(C));
- return reusableVec4
+ reusableVec1$1.set(getX(A), getY(A), getZ(A));
+ reusableVec2$1.set(getX(B), getY(B), getZ(B));
+ reusableVec3$1.set(getX(C), getY(C), getZ(C));
+ return reusableVec4$1
.crossVectors(
- reusableVec2.sub(reusableVec1),
- reusableVec3.sub(reusableVec1)
+ reusableVec2$1.sub(reusableVec1$1),
+ reusableVec3$1.sub(reusableVec1$1)
)
.clone();
}
@@ -723,29 +1025,6 @@
return -(Math.cos(Math.PI * x) - 1) / 2;
}
- /** @param {Mesh} mesh */
- function computeVertexNeighborhood(mesh) {
- const map = {};
-
- for (const key in mesh.faces) {
- const face = mesh.faces[key];
-
- face.vertices.forEach((vkey) => {
- if (!(vkey in map)) {
- map[vkey] = [];
- }
-
- face.vertices.forEach((neighborkey) => {
- if (neighborkey == vkey) return;
-
- map[vkey].safePush(neighborkey);
- });
- });
- }
-
- return map;
- }
-
function getAdjacentElements(arr, index) {
return [
arr[(index + 1 + arr.length) % arr.length],
@@ -837,22 +1116,21 @@
if (polygonOrPoint instanceof Array) {
return polygonOrPoint.map((e) => {
- reusableVec5.copy(e);
- reusableVec5.applyQuaternion(quat);
- return new THREE.Vector2(reusableVec5.x, reusableVec5.z);
+ reusableVec5$1.copy(e);
+ reusableVec5$1.applyQuaternion(quat);
+ return new THREE.Vector2(reusableVec5$1.x, reusableVec5$1.z);
});
}
- reusableVec5.copy(polygonOrPoint);
- reusableVec5.applyQuaternion(quat);
- return new THREE.Vector2(reusableVec5.x, reusableVec5.z);
+ reusableVec5$1.copy(polygonOrPoint);
+ reusableVec5$1.applyQuaternion(quat);
+ return new THREE.Vector2(reusableVec5$1.x, reusableVec5$1.z);
}
/**
* Triangulates a polygon into a set of triangles.
*
* @param {ArrayVector3[]} polygon
- * @returns {Array} An array of triangles. Each triangle is represented
- * as an ArrayVector3
+ * @returns {Array} An array of triangles.
*/
function triangulate(polygon) {
const vertices3d = polygon.map((v) => v.V3_toThree());
@@ -941,20 +1219,6 @@
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(" ");
}
-
- function minIndex(array) {
- let minI = -1;
- let minValue = Infinity;
- for (let i = 0; i < array.length; i++) {
- const value = array[i];
-
- if (value <= minValue) {
- minValue = value;
- minI = i;
- }
- }
- return minI;
- }
/**
*
* @param {ArrayVector3[]} points
@@ -1068,6 +1332,11 @@
}
return `${a}_${b}`;
}
+ /**
+ *
+ * @param {string} edgeKey
+ * @returns {[string, string]}
+ */
function extractEdgeKey(edgeKey) {
return edgeKey.split("_");
}
@@ -1079,7 +1348,7 @@
const selectedConnectedCount = {};
const connectedCount = {};
- const neighborhood = computeEdgeFacesNeighborhood(mesh);
+ const neighborhood = Neighborhood.EdgeFaces(mesh);
for (const [a, b] of edges) {
const edgeKey = getEdgeKey(a, b);
@@ -1097,67 +1366,7 @@
}
return { connectedCount, selectedConnectedCount };
}
- /**
- *
- * @param {Mesh} mesh
- * @returns {{[edgeKey: string]: MeshFace[]}}
- */
- function computeEdgeFacesNeighborhood(mesh) {
- const neighborhood = {};
- for (const key in mesh.faces) {
- const face = mesh.faces[key];
- const vertices = face.getSortedVertices();
-
- for (let i = 0; i < vertices.length; i++) {
- const vertexCurr = vertices[i];
- const vertexNext = vertices[(i + 1) % vertices.length];
- const edgeKey = getEdgeKey(vertexCurr, vertexNext);
- neighborhood[edgeKey] ??= [];
- neighborhood[edgeKey].safePush(face);
- }
- }
- return neighborhood;
- }
-
- /**
- *
- * @param {ArrayVector3} a
- * @param {ArrayVector3} b
- * @param {number} t
- * @returns {ArrayVector3}
- */
- function lerp3(a, b, t) {
- return a.map((e, i) => Math.lerp(e, b[i], t));
- }
-
- function groupElementsCollided(array, every = 2) {
- const newArray = [];
- for (let i = 0; i < array.length; i++) {
- const sub = [];
- for (let j = 0; j < every; j++) {
- const element = array[(i + j) % array.length];
- sub.push(element);
- }
- newArray.push(sub);
- }
- return newArray;
- }
-
- function findMin(array, map = (x) => x) {
- let minElement = null;
- let minValue = Infinity;
-
- for (const element of array) {
- const value = map(element);
-
- if (value <= minValue) {
- minElement = element;
- minValue = value;
- }
- }
- return minElement;
- }
function computeCentroid(polygon) {
const centroid = new THREE.Vector3();
for (const vertex of polygon) {
@@ -1167,17 +1376,6 @@
return centroid;
}
- function offsetArray(array, offset) {
- while (offset < 0) offset += array.length;
- while (offset >= array.length) offset -= array.length;
-
- const newArr = [];
- for (let i = 0; i < array.length; i++) {
- newArr[(i + offset) % array.length] = array[i];
- }
-
- array.splice(0, Infinity, ...newArr);
- }
/**
*
@@ -1227,16 +1425,6 @@
t ** 3 * p3
);
}
- /**
- *
- * @param {string} message
- * @param {?number} timeout
- * @returns {never}
- */
- function throwQuickMessage(message, timeout) {
- Blockbench.showQuickMessage(message, timeout);
- throw message;
- }
/**
* !
@@ -1335,7 +1523,7 @@
edgeLoopB,
centroidA,
centroidB,
- { twist, numberOfCuts, blendPath, blendInfluence }
+ { twist, numberOfCuts, blendPath, blendInfluence, reverse }
) {
if (edgeLoopA.length < 3 || edgeLoopB.length < 3) {
return;
@@ -1343,24 +1531,20 @@
edgeLoopA = edgeLoopA.map((e) => e.slice());
edgeLoopB = edgeLoopB.map((e) => e.slice());
- const bestOffset = bestEdgeLoopsOffset(edgeLoopB, edgeLoopA, mesh);
+ const bestOffset = bestEdgeLoopsOffset(edgeLoopA, edgeLoopB, mesh);
offsetArray(edgeLoopB, bestOffset);
const reversedEdgeLoopB = edgeLoopB.map((e) => e.slice().reverse()).reverse();
const bestOffsetReversed = bestEdgeLoopsOffset(
- reversedEdgeLoopB,
edgeLoopA,
+ reversedEdgeLoopB,
mesh
);
- // Negation of `bestOffset2` since the array is reversed,
- // Does it make ANY sense?
- // It doesn't!
- // It just happens to work.
- offsetArray(reversedEdgeLoopB, -bestOffsetReversed);
+ offsetArray(reversedEdgeLoopB, bestOffsetReversed);
if (
- edgeLoopsLength(mesh, edgeLoopA, edgeLoopB) >
- edgeLoopsLength(mesh, edgeLoopA, reversedEdgeLoopB)
+ edgeLoopsLength(mesh, edgeLoopA, reverse ? edgeLoopB : reversedEdgeLoopB) <
+ edgeLoopsLength(mesh, edgeLoopA, reverse ? reversedEdgeLoopB : edgeLoopB)
) {
edgeLoopB = reversedEdgeLoopB;
}
@@ -1487,6 +1671,7 @@
cutHoles,
blendPath,
blendInfluence,
+ reverse
) {
Undo.initEdit({ elements: Mesh.selected, selection: true }, amend);
@@ -1589,7 +1774,11 @@
};
}
- const sortedEdgeLoops = [loops.pop()];
+ const furthestLoop = findMin(loops, e => e.centroid.length());
+ loops.remove(furthestLoop);
+
+ const sortedEdgeLoops = [furthestLoop];
+ mesh.addVertices(sortedEdgeLoops[0].centroid.toArray());
while (loops.length) {
const currEdgeLoop = sortedEdgeLoops.last();
const closestLoop = findMin(loops, (e) =>
@@ -1615,6 +1804,7 @@
numberOfCuts,
blendPath,
blendInfluence,
+ reverse
}
);
}
@@ -1626,15 +1816,15 @@
Undo.finishEdit("MTools: Bridged Edge Loops.");
}
action("bridge_edge_loops", () => {
- runEdit$c(false, 2, 0, true, true, 1);
+ runEdit$c(false, 2, 0, true, true, 1, false);
Undo.amendEdit(
{
- blend_path: {
- type: "checkbox",
- label: "Blend Path",
- value: true,
- },
+ // reverse: {
+ // type: "checkbox",
+ // label: "Reverse Winding",
+ // value: false,
+ // },
blend_influence: {
type: "number",
label: "Smoothness",
@@ -1653,6 +1843,11 @@
label: "Twist",
value: 0,
},
+ blend_path: {
+ type: "checkbox",
+ label: "Blend Path",
+ value: true,
+ },
cut_holes: {
type: "checkbox",
label: "Cut Holes",
@@ -1668,6 +1863,7 @@
form.cut_holes,
form.blend_path,
form.blend_influence / 100,
+ form.reverse
);
}
);
@@ -1718,7 +1914,7 @@
action("expand_selection", () => {
Mesh.selected.forEach((mesh) => {
- const neighborMap = computeVertexNeighborhood(mesh);
+ const neighborMap = Neighborhood.VertexVertices(mesh);
const selectedVertices = mesh.getSelectedVertices();
const selectedVertexSet = new Set(selectedVertices);
@@ -1742,7 +1938,7 @@
if (!influence || !iterations) return; //
const { vertices } = mesh;
- const neighborMap = computeVertexNeighborhood(mesh);
+ const neighborMap = Neighborhood.VertexVertices(mesh);
const selectedVertices = mesh.getSelectedVertices();
@@ -1849,7 +2045,7 @@
action("shrink_selection", () => {
Mesh.selected.forEach((mesh) => {
- const neighborMap = computeVertexNeighborhood(mesh);
+ const neighborMap = Neighborhood.VertexVertices(mesh);
const selectedVertices = mesh.getSelectedVertices();
const selectedVertexSet = new Set(selectedVertices);
@@ -2354,10 +2550,131 @@
);
});
+ const LanguageDefinitions = {
+ "word.before": "Input",
+ "word.after": "Result",
+ "word.mesh": "Mesh",
+ "word.uv": "UV",
+ };
+ function getLanguage() {
+ return LanguageDefinitions;
+ }
+ function translate(subject) {
+ return subject.replace(/[a-zA-Z_][a-zA-Z0-9_\.]+/g, (key) => {
+ return getLanguage()[key] ?? key;
+ });
+
+ }
+ const getURL = (e) =>
+ // `http://127.0.0.1:5500/${e}?t=${Math.random()}`;
+ `https://github.com/Malik12tree/blockbench-plugins/blob/master/src/mesh_tools/${e}?raw=true`;
+ function renderImage({ src, caption = "" }) {
+ return `
+
+ `;
+ }
+ function renderOverflow(children) {
+ return `${children}`;
+ }
+ function renderInsetRow({ items }) {
+ return `
+ ${items
+ .map(
+ (e) =>
+ `
${renderLine(
+ e
+ )}
`
+ )
+ .join("\n")}
+
+ `;
+ }
+ function renderLine(options) {
+ if (typeof options == "string") return options;
+
+ switch (options.type) {
+ case "image":
+ return renderImage(options);
+ case "overflow":
+ return renderOverflow(options);
+ case "inset_row":
+ return renderInsetRow(options);
+ default:
+ throw new Error(`Unknown line type: ${options.type}`);
+ }
+ }
+
+ const dontShowAgainInfoStorage = storage.in("dont_show_again_info_storage");
+ function dontShowAgainInfo(id, title, message) {
+ if (dontShowAgainInfoStorage.has(id)) {
+ return;
+ }
+
+ const messageBox = Blockbench.showMessageBox(
+ {
+ title,
+ message,
+ icon: "info",
+ checkboxes: {
+ dont_show_again: { value: false, text: "dialog.dontshowagain" },
+ },
+ buttons: ["dialog.ok"],
+ },
+ (_, { dont_show_again: dontShowAgain }) => {
+ if (dontShowAgain) {
+ dontShowAgainInfoStorage.set(id, true);
+ }
+ }
+ );
+ messageBox.object.querySelector(".dialog_content").style.overflow = "auto";
+ }
+
+ /**
+ *
+ * @param {string} message
+ * @param {?number} timeout
+ * @returns {never}
+ */
+ function throwQuickMessage(message, timeout) {
+ Blockbench.showQuickMessage(message, timeout);
+ throw message;
+ }
+
const reusableEuler1 = new THREE.Euler();
- function runEdit$4(mesh,selected, group, density, amend = false) {
+ const reusableVec1 = new THREE.Vector3();
+ const reusableVec2 = new THREE.Vector3();
+ const reusableVec3 = new THREE.Vector3();
+ const reusableVec4 = new THREE.Vector3();
+ const reusableVec5 = new THREE.Vector3();
+ // const reusableVec6 = new THREE.Vector3();
+ function runEdit$4(
+ mesh,
+ selected,
+ {
+ density = 3,
+ min_distance: minDistance = 0,
+ scale = 100,
+ min_scale: minScale = 100,
+ scale_factor: scaleFactor = 50,
+ rotation = 0,
+ rotation_factor: rotationFactor = 0,
+ },
+ amend = false
+ ) {
const meshes = [];
- Undo.initEdit({ elements: meshes, selection: true, group }, amend);
+ scale /= 100;
+ minScale /= 100;
+ scaleFactor /= 100;
+ rotationFactor /= 100;
+ const minDistanceSquared = minDistance ** 2;
+
+ Undo.initEdit({ outliner: true, elements: [], selection: true }, amend);
+
/**
* @type {THREE.Mesh}
*/
@@ -2367,19 +2684,20 @@
const vertices = tmesh.geometry.getAttribute("position");
const l = faces.count;
+ const points = [];
for (let d = 0; d < density; d++) {
const i = Math.floor((Math.random() * l) / 3) * 3; // random face index
- const t0 = new THREE.Vector3(
+ const t0 = reusableVec1.set(
vertices.getX(faces.getX(i)),
vertices.getY(faces.getX(i)),
vertices.getZ(faces.getX(i))
);
- const t1 = new THREE.Vector3(
+ const t1 = reusableVec2.set(
vertices.getX(faces.getY(i)),
vertices.getY(faces.getY(i)),
vertices.getZ(faces.getY(i))
);
- const t2 = new THREE.Vector3(
+ const t2 = reusableVec3.set(
vertices.getX(faces.getZ(i)),
vertices.getY(faces.getZ(i)),
vertices.getZ(faces.getZ(i))
@@ -2390,64 +2708,156 @@
tmesh.localToWorld(t2);
// f*ed up midpoint theroem
- const pointA = new THREE.Vector3().lerpVectors(t0, t1, Math.random());
- const pointB = new THREE.Vector3().lerpVectors(t0, t2, Math.random());
- const pointF = new THREE.Vector3().lerpVectors(
+ const pointA = reusableVec4.lerpVectors(t0, t1, Math.random());
+ const pointB = reusableVec5.lerpVectors(t0, t2, Math.random());
+
+ const point = new THREE.Vector3().lerpVectors(
pointA,
pointB,
Math.random()
);
-
+ if (points.find((e) => e.distanceToSquared(point) < minDistanceSquared)) {
+ continue;
+ }
+ points.push(point);
// scatter on points
+ /**
+ * @type {Mesh}
+ */
const otherMesh =
selected[Math.floor(selected.length * Math.random())].duplicate();
otherMesh.removeFromParent();
otherMesh.parent = "root";
- Outliner.root.push(otherMesh);
+
+ const currentScale = Math.lerp(
+ scale,
+ Math.lerp(minScale, 1, Math.random()) * scale,
+ scaleFactor
+ );
+
+ const currentRotation = rotationFactor * (Math.random() * 2 - 1) * rotation;
+
+ for (const key in otherMesh.vertices) {
+ otherMesh.vertices[key].V3_multiply(currentScale);
+ }
const normal = computeTriangleNormal(t0, t1, t2);
- const rotation = rotationFromDirection(normal, reusableEuler1);
- otherMesh.rotation[0] = Math.radToDeg(rotation.x);
- otherMesh.rotation[1] = Math.radToDeg(rotation.y);
- otherMesh.rotation[2] = Math.radToDeg(rotation.z);
+ const euler = rotationFromDirection(normal, reusableEuler1, {
+ rotateY: Math.degToRad(currentRotation),
+ });
+ otherMesh.rotation[0] = Math.radToDeg(euler.x);
+ otherMesh.rotation[1] = Math.radToDeg(euler.y);
+ otherMesh.rotation[2] = Math.radToDeg(euler.z);
- otherMesh.origin = pointF.toArray();
+ otherMesh.origin = point.toArray();
- otherMesh.addTo(group);
meshes.push(otherMesh);
}
- Undo.finishEdit("MTools: Scatter meshes");
- Canvas.updatePositions();
+ const group = new Group({ name: "instances_on_" + mesh.name });
+ meshes.forEach((e) => {
+ // Outliner.root.push(otherMesh);
+ e.addTo(group);
+ });
+ group.init();
+
+ Undo.finishEdit("MTools: Scatter meshes", {
+ outliner: true,
+ elements: meshes,
+ selection: true,
+ });
+ Canvas.updateAll();
}
action("scatter", function () {
if (Mesh.selected.length < 2) {
Blockbench.showQuickMessage("At least two meshes must be selected");
return;
}
+ dontShowAgainInfo(
+ "scatter_pivot",
+ "Good To Know",
+ [
+ "Scattered meshes are relative to their pivot points on the target surface.",
+
+ renderLine({
+ type: "inset_row",
+ items: [
+ {
+ type: "image",
+ src: "scatter_pivot_1.png",
+ caption: "Pivot point located on the bottom",
+ },
+ {
+ type: "image",
+ src: "scatter_pivot_2.png",
+ caption: "Pivot point located far down",
+ },
+ ],
+ }),
+ ].join("\n")
+ );
const mesh = Mesh.selected.last();
mesh.unselect();
- const group = new Group({ name: "instances_on_" + mesh.name });
- group.init();
-
const selected = Mesh.selected.slice();
- runEdit$4(mesh, selected, group, 3);
+ runEdit$4(mesh, selected, {});
Undo.amendEdit(
{
density: {
type: "number",
value: 3,
- label: "Density",
+ label: "Max Density",
+ min: 0,
+ max: 100,
+ },
+ min_distance: {
+ type: "number",
+ value: 0,
+ label: "Min Distance",
+ min: 0,
+ },
+ scale: {
+ type: "number",
+ value: 100,
+ label: "Scale",
+ min: 0,
+ max: 100,
+ },
+ min_scale: {
+ type: "number",
+ value: 100,
+ label: "Min Scale",
+ min: 0,
+ max: 100,
+ },
+ scale_factor: {
+ type: "number",
+ value: 50,
+ label: "Scale Randomness",
+ min: 0,
+ max: 100,
+ },
+
+ rotation: {
+ type: "number",
+ value: 0,
+ label: "Max Rotation",
+ min: 0,
+ max: 180,
+ },
+ rotation_factor: {
+ type: "number",
+ value: 0,
+ label: "Rotation Randomness",
min: 0,
max: 100,
},
},
(form) => {
- runEdit$4(mesh, selected, group, form.density, true);
+ runEdit$4(mesh, selected, form, true);
}
);
});
@@ -19805,7 +20215,7 @@
}
}
for (const char of out.text) {
- if (!(char in content.glyphs)) {
+ if (char != '\n' && !(char in content.glyphs)) {
throwQuickMessage(
`Character "${char}" doesn't exist on the provided font!`,
2000
@@ -22531,7 +22941,6 @@
elements.push(mesh);
mesh.select();
UVEditor.setAutoSize(null, true, Object.keys(mesh.faces));
- UVEditor.selected_faces.empty();
Undo.finishEdit("MTools: Generate Mesh");
}
const dialog$1 = new Dialog({
@@ -22592,7 +23001,6 @@
elements.push(mesh);
mesh.select();
UVEditor.setAutoSize(null, true, Object.keys(mesh.faces));
- UVEditor.selected_faces.empty();
Undo.finishEdit("MTools: Generate Mesh");
}
const dialog = new Dialog({
@@ -22643,21 +23051,25 @@
* @type {Array}
*/
const meshToolTips = [];
- BBPlugin.register("mesh_tools", {
- "new_repository_format": true,
- "title": "MTools",
- "author": "Malik12tree",
- "icon": "icon.png",
- "description": "Adds powerful mesh modeling tools, operators and generators!",
- "version": "2.0.0",
- "min_version": "4.9.4",
- "variant": "both",
- "tags": ["Format: Generic Model", "Mesh", "Tool"],
+ BBPlugin.register(PLUGIN_ID, {
+ new_repository_format: true,
+ title: "MTools",
+ author: "Malik12tree",
+ icon: "icon.png",
+ description: "Adds powerful mesh modeling tools, operators and generators!",
+ version: "2.0.1",
+ min_version: "4.9.4",
+ variant: "both",
+ creation_date: "2022-04-09",
+ tags: ["Format: Generic Model", "Mesh", "Tool"],
onload() {
- Mesh.prototype.menu.structure.unshift("@meshtools/tools");
- Mesh.prototype.menu.structure.unshift("@meshtools/operators");
- MenuBar.addAction("@meshtools/generators", "filter");
+ Mesh.prototype.menu.structure.unshift(qualifyName("tools"));
+ Mesh.prototype.menu.structure.unshift(qualifyName("operators"));
+ MenuBar.addAction(qualifyName("generators"), "filter");
+ },
+ onuninstall() {
+ storage.clear();
},
onunload() {
for (const deletable of deletables) {
diff --git a/src/mesh_tools/.gitignore b/src/mesh_tools/.gitignore
index e69de29b..b7265688 100644
--- a/src/mesh_tools/.gitignore
+++ b/src/mesh_tools/.gitignore
@@ -0,0 +1 @@
+examples
\ No newline at end of file
diff --git a/src/mesh_tools/.vscode/settings.json b/src/mesh_tools/.vscode/settings.json
index f8222ec0..46390b30 100644
--- a/src/mesh_tools/.vscode/settings.json
+++ b/src/mesh_tools/.vscode/settings.json
@@ -1,6 +1,9 @@
{
"cSpell.words": [
- "perlin"
+ "dont",
+ "perlin",
+ "quadrilate",
+ "Quadrilates"
],
"files.autoSave": "onWindowChange"
}
\ No newline at end of file
diff --git a/src/mesh_tools/TODO.md b/src/mesh_tools/TODO.md
index c5cae4d2..081a04e1 100644
--- a/src/mesh_tools/TODO.md
+++ b/src/mesh_tools/TODO.md
@@ -24,7 +24,16 @@
- [x] Triangles To Quads: Improved Algorithm
- [x] Expand/Shrink Selection: Enable for face and edge modes.
- [ ] MTools Operators:
- - [x] Scatter: Improve scattered mesh rotation to accurately follow face
+ - [x] Scatter
+ - [x] Improve scattered mesh rotation to accurately follow face
+ - [x] Add More Settings
+ - [x] `Max Density`
+ - [x] `Min Distance`
+ - [x] `Scale`
+ - [x] `Min Scale`
+ - [x] `Scale Randomness`
+ - [x] `Max Rotation`
+ - [x] `Rotation Randomness`
- [ ] Array operator: add more controls like rotation and scale
- [ ] New Decimate Operator
- [ ] Unite MTEdge and CMEdge
diff --git a/src/mesh_tools/assets/actions.json b/src/mesh_tools/assets/actions.json
index fd2de4b2..82792202 100644
--- a/src/mesh_tools/assets/actions.json
+++ b/src/mesh_tools/assets/actions.json
@@ -1,4 +1,15 @@
{
+ "bevel": {
+ "docs": {
+ "private": true
+ },
+ "name": "Bevel",
+ "icon": "rounded_corner",
+ "description": "Chamfers selected edges",
+ "selection_mode": [
+ "edge"
+ ]
+ },
"laplacian_smooth": {
"docs": {
"lines": [
diff --git a/src/mesh_tools/assets/actions/scatter.png b/src/mesh_tools/assets/actions/scatter.png
index 765c8ceb..73b25f6f 100644
Binary files a/src/mesh_tools/assets/actions/scatter.png and b/src/mesh_tools/assets/actions/scatter.png differ
diff --git a/src/mesh_tools/assets/actions/scatter_pivot_1.png b/src/mesh_tools/assets/actions/scatter_pivot_1.png
new file mode 100644
index 00000000..353254d5
Binary files /dev/null and b/src/mesh_tools/assets/actions/scatter_pivot_1.png differ
diff --git a/src/mesh_tools/assets/actions/scatter_pivot_2.png b/src/mesh_tools/assets/actions/scatter_pivot_2.png
new file mode 100644
index 00000000..3c6c3323
Binary files /dev/null and b/src/mesh_tools/assets/actions/scatter_pivot_2.png differ
diff --git a/src/mesh_tools/builders/about.plugin.js b/src/mesh_tools/builders/about.plugin.js
index 7d6a27a3..6a13ffb9 100644
--- a/src/mesh_tools/builders/about.plugin.js
+++ b/src/mesh_tools/builders/about.plugin.js
@@ -1,75 +1,10 @@
import { promises as fs } from "fs";
import path from "path";
-
-const LanguageDefinitions = {
- "word.before": "Input",
- "word.after": "Result",
- "word.mesh": "Mesh",
- "word.uv": "UV",
-};
-function getLanguage() {
- return LanguageDefinitions;
-}
-function translate(subject) {
- return subject.replace(/[a-zA-Z_][a-zA-Z0-9_\.]+/g, (key) => {
- return getLanguage(LanguageDefinitions)[key] ?? key;
- });
-}
-const getURL = (e) =>
- // `http://127.0.0.1:5500/${e}`;
- `https://github.com/Malik12tree/blockbench-plugins/blob/master/src/mesh_tools/${e}?raw=true`;
-
-function renderPill(title) {
- return `${title.toString().toUpperCase()}`
-}
-function renderImage({ src, caption = "" }) {
- return `
-
-`;
-}
-function renderInsetRow({ items }) {
- return `
-
- ${items
- .map(
- (e) =>
- `
${renderLine(
- e
- )}
`
- )
- .join("\n")}
-
- `;
-}
-function renderLine(options) {
- if (typeof options == "string") return options;
-
- switch (options.type) {
- case "image":
- return renderImage(options);
- case "inset_row":
- return renderInsetRow(options);
- default:
- throw new Error(`Unknown line type: ${options.type}`);
- break;
- }
-}
+import { renderLine, renderPill } from "../src/utils/docs.js";
let lastRawActions = "";
export default {
- buildStart: async function (aa) {
+ buildStart: async function () {
const rawActions = await fs.readFile(path.resolve("assets/actions.json"), {
encoding: "utf-8",
});
@@ -82,6 +17,7 @@ export default {
for (const id in ACTIONS) {
const action = ACTIONS[id];
action.id = id;
+ if (ACTIONS[id]?.docs?.private) delete ACTIONS[id];
}
const tableOfContents = [];
diff --git a/src/mesh_tools/src/actions.js b/src/mesh_tools/src/actions.js
index f9e18fe3..8a009b02 100644
--- a/src/mesh_tools/src/actions.js
+++ b/src/mesh_tools/src/actions.js
@@ -1,4 +1,5 @@
import _ACTIONS from '../assets/actions.json';
+import { PLUGIN_ID } from './globals.js';
export const ACTIONS = _ACTIONS;
const CONDITIONS = {
@@ -21,7 +22,7 @@ const CONDITIONS = {
}
-export const qualifyName = (id) => (id == "_" ? id : `@meshtools/${id}`);
+export const qualifyName = (id) => (id == "_" ? id : `@${PLUGIN_ID}/${id}`);
/**
*
diff --git a/src/mesh_tools/src/generators/quickprimitives/polyhedron.action.js b/src/mesh_tools/src/generators/quickprimitives/polyhedron.action.js
index e9d526ae..ef1deb11 100644
--- a/src/mesh_tools/src/generators/quickprimitives/polyhedron.action.js
+++ b/src/mesh_tools/src/generators/quickprimitives/polyhedron.action.js
@@ -14,7 +14,6 @@ function runEdit(selected, s, amended = false) {
elements.push(mesh);
mesh.select();
UVEditor.setAutoSize(null, true, Object.keys(mesh.faces));
- UVEditor.selected_faces.empty();
Undo.finishEdit("MTools: Generate Mesh");
}
const dialog = new Dialog({
diff --git a/src/mesh_tools/src/generators/quickprimitives/torusknot.action.js b/src/mesh_tools/src/generators/quickprimitives/torusknot.action.js
index 15cd6fb2..fe22060f 100644
--- a/src/mesh_tools/src/generators/quickprimitives/torusknot.action.js
+++ b/src/mesh_tools/src/generators/quickprimitives/torusknot.action.js
@@ -18,7 +18,6 @@ function runEdit(s, amended = false) {
elements.push(mesh);
mesh.select();
UVEditor.setAutoSize(null, true, Object.keys(mesh.faces));
- UVEditor.selected_faces.empty();
Undo.finishEdit("MTools: Generate Mesh");
}
const dialog = new Dialog({
diff --git a/src/mesh_tools/src/generators/text_mesh.action.js b/src/mesh_tools/src/generators/text_mesh.action.js
index 4653eaa4..7e0cbdb4 100644
--- a/src/mesh_tools/src/generators/text_mesh.action.js
+++ b/src/mesh_tools/src/generators/text_mesh.action.js
@@ -1,8 +1,8 @@
import RobotoRegular from "../../assets/roboto_regular.json";
import { action } from "../actions.js";
import { convertOpenTypeBufferToThreeJS } from "../utils/facetype.js";
+import { throwQuickMessage } from "../utils/info.js";
import * as ThreeJSInteroperability from "../utils/threejs_interoperability.js";
-import { throwQuickMessage } from "../utils/utils.js";
function runEdit(text, font, s, amended = false) {
let elements = [];
@@ -85,7 +85,7 @@ const dialog = new Dialog({
}
}
for (const char of out.text) {
- if (!(char in content.glyphs)) {
+ if (char != '\n' && !(char in content.glyphs)) {
throwQuickMessage(
`Character "${char}" doesn't exist on the provided font!`,
2000
diff --git a/src/mesh_tools/src/globals.js b/src/mesh_tools/src/globals.js
new file mode 100644
index 00000000..ae6182ac
--- /dev/null
+++ b/src/mesh_tools/src/globals.js
@@ -0,0 +1,4 @@
+import { QualifiedStorage } from "./utils/storage.js";
+
+export const PLUGIN_ID = "mesh_tools";
+export const storage = new QualifiedStorage(PLUGIN_ID);
diff --git a/src/mesh_tools/src/index.js b/src/mesh_tools/src/index.js
index 9d72f349..312bfb3a 100644
--- a/src/mesh_tools/src/index.js
+++ b/src/mesh_tools/src/index.js
@@ -3,22 +3,24 @@ import "./operators/index.js";
import "./generators/index.js";
import { ACTIONS, qualifyName } from "./actions.js";
import { createTextMesh, vertexNormal } from "./utils/utils.js";
+import { PLUGIN_ID, storage } from "./globals.js";
const deletables = [];
/**
* @type {Array}
*/
const meshToolTips = [];
-BBPlugin.register("mesh_tools", {
- "new_repository_format": true,
- "title": "MTools",
- "author": "Malik12tree",
- "icon": "icon.png",
- "description": "Adds powerful mesh modeling tools, operators and generators!",
- "version": "2.0.0",
- "min_version": "4.9.4",
- "variant": "both",
- "tags": ["Format: Generic Model", "Mesh", "Tool"],
+BBPlugin.register(PLUGIN_ID, {
+ new_repository_format: true,
+ title: "MTools",
+ author: "Malik12tree",
+ icon: "icon.png",
+ description: "Adds powerful mesh modeling tools, operators and generators!",
+ version: "2.0.1",
+ min_version: "4.9.4",
+ variant: "both",
+ creation_date: "2022-04-09",
+ tags: ["Format: Generic Model", "Mesh", "Tool"],
onload() {
function debug(element) {
for (const object of meshToolTips) {
@@ -40,7 +42,6 @@ BBPlugin.register("mesh_tools", {
mesh.position.copy(center);
if (normal) {
-
mesh.position.add(normal.multiplyScalar(0.5));
}
@@ -70,7 +71,7 @@ BBPlugin.register("mesh_tools", {
}
// TODO move to separate plugin
- const isDebug =false && this.source == "file";
+ const isDebug = false && this.source == "file";
if (isDebug) {
deletables.push(
Mesh.prototype.preview_controller.on("update_geometry", ({ element }) =>
@@ -82,9 +83,12 @@ BBPlugin.register("mesh_tools", {
}
}
- Mesh.prototype.menu.structure.unshift("@meshtools/tools");
- Mesh.prototype.menu.structure.unshift("@meshtools/operators");
- MenuBar.addAction("@meshtools/generators", "filter");
+ Mesh.prototype.menu.structure.unshift(qualifyName("tools"));
+ Mesh.prototype.menu.structure.unshift(qualifyName("operators"));
+ MenuBar.addAction(qualifyName("generators"), "filter");
+ },
+ onuninstall() {
+ storage.clear();
},
onunload() {
for (const deletable of deletables) {
diff --git a/src/mesh_tools/src/operators/scatter.action.js b/src/mesh_tools/src/operators/scatter.action.js
index bd207b50..a694c5d3 100644
--- a/src/mesh_tools/src/operators/scatter.action.js
+++ b/src/mesh_tools/src/operators/scatter.action.js
@@ -1,10 +1,41 @@
import { action } from "../actions.js";
-import { computeTriangleNormal, rotationFromDirection } from "../utils/utils.js";
+import { renderLine } from "../utils/docs.js";
+import { dontShowAgainInfo } from "../utils/info.js";
+import {
+ computeTriangleNormal,
+ rotationFromDirection,
+} from "../utils/utils.js";
const reusableEuler1 = new THREE.Euler();
-function runEdit(mesh,selected, group, density, amend = false) {
+const reusableVec1 = new THREE.Vector3();
+const reusableVec2 = new THREE.Vector3();
+const reusableVec3 = new THREE.Vector3();
+const reusableVec4 = new THREE.Vector3();
+const reusableVec5 = new THREE.Vector3();
+// const reusableVec6 = new THREE.Vector3();
+function runEdit(
+ mesh,
+ selected,
+ {
+ density = 3,
+ min_distance: minDistance = 0,
+ scale = 100,
+ min_scale: minScale = 100,
+ scale_factor: scaleFactor = 50,
+ rotation = 0,
+ rotation_factor: rotationFactor = 0,
+ },
+ amend = false
+) {
const meshes = [];
- Undo.initEdit({ elements: meshes, selection: true, group }, amend);
+ scale /= 100;
+ minScale /= 100;
+ scaleFactor /= 100;
+ rotationFactor /= 100;
+ const minDistanceSquared = minDistance ** 2;
+
+ Undo.initEdit({ outliner: true, elements: [], selection: true }, amend);
+
/**
* @type {THREE.Mesh}
*/
@@ -14,19 +45,20 @@ function runEdit(mesh,selected, group, density, amend = false) {
const vertices = tmesh.geometry.getAttribute("position");
const l = faces.count;
+ const points = [];
for (let d = 0; d < density; d++) {
const i = Math.floor((Math.random() * l) / 3) * 3; // random face index
- const t0 = new THREE.Vector3(
+ const t0 = reusableVec1.set(
vertices.getX(faces.getX(i)),
vertices.getY(faces.getX(i)),
vertices.getZ(faces.getX(i))
);
- const t1 = new THREE.Vector3(
+ const t1 = reusableVec2.set(
vertices.getX(faces.getY(i)),
vertices.getY(faces.getY(i)),
vertices.getZ(faces.getY(i))
);
- const t2 = new THREE.Vector3(
+ const t2 = reusableVec3.set(
vertices.getX(faces.getZ(i)),
vertices.getY(faces.getZ(i)),
vertices.getZ(faces.getZ(i))
@@ -37,64 +69,156 @@ function runEdit(mesh,selected, group, density, amend = false) {
tmesh.localToWorld(t2);
// f*ed up midpoint theroem
- const pointA = new THREE.Vector3().lerpVectors(t0, t1, Math.random());
- const pointB = new THREE.Vector3().lerpVectors(t0, t2, Math.random());
- const pointF = new THREE.Vector3().lerpVectors(
+ const pointA = reusableVec4.lerpVectors(t0, t1, Math.random());
+ const pointB = reusableVec5.lerpVectors(t0, t2, Math.random());
+
+ const point = new THREE.Vector3().lerpVectors(
pointA,
pointB,
Math.random()
);
-
+ if (points.find((e) => e.distanceToSquared(point) < minDistanceSquared)) {
+ continue;
+ }
+ points.push(point);
// scatter on points
+ /**
+ * @type {Mesh}
+ */
const otherMesh =
selected[Math.floor(selected.length * Math.random())].duplicate();
otherMesh.removeFromParent();
otherMesh.parent = "root";
- Outliner.root.push(otherMesh);
+
+ const currentScale = Math.lerp(
+ scale,
+ Math.lerp(minScale, 1, Math.random()) * scale,
+ scaleFactor
+ );
+
+ const currentRotation = rotationFactor * (Math.random() * 2 - 1) * rotation;
+
+ for (const key in otherMesh.vertices) {
+ otherMesh.vertices[key].V3_multiply(currentScale);
+ }
const normal = computeTriangleNormal(t0, t1, t2);
- const rotation = rotationFromDirection(normal, reusableEuler1);
- otherMesh.rotation[0] = Math.radToDeg(rotation.x);
- otherMesh.rotation[1] = Math.radToDeg(rotation.y);
- otherMesh.rotation[2] = Math.radToDeg(rotation.z);
+ const euler = rotationFromDirection(normal, reusableEuler1, {
+ rotateY: Math.degToRad(currentRotation),
+ });
+ otherMesh.rotation[0] = Math.radToDeg(euler.x);
+ otherMesh.rotation[1] = Math.radToDeg(euler.y);
+ otherMesh.rotation[2] = Math.radToDeg(euler.z);
- otherMesh.origin = pointF.toArray();
+ otherMesh.origin = point.toArray();
- otherMesh.addTo(group);
meshes.push(otherMesh);
}
- Undo.finishEdit("MTools: Scatter meshes");
- Canvas.updatePositions();
+ const group = new Group({ name: "instances_on_" + mesh.name });
+ meshes.forEach((e) => {
+ // Outliner.root.push(otherMesh);
+ e.addTo(group);
+ });
+ group.init();
+
+ Undo.finishEdit("MTools: Scatter meshes", {
+ outliner: true,
+ elements: meshes,
+ selection: true,
+ });
+ Canvas.updateAll();
}
export default action("scatter", function () {
if (Mesh.selected.length < 2) {
Blockbench.showQuickMessage("At least two meshes must be selected");
return;
}
+ dontShowAgainInfo(
+ "scatter_pivot",
+ "Good To Know",
+ [
+ "Scattered meshes are relative to their pivot points on the target surface.",
+
+ renderLine({
+ type: "inset_row",
+ items: [
+ {
+ type: "image",
+ src: "scatter_pivot_1.png",
+ caption: "Pivot point located on the bottom",
+ },
+ {
+ type: "image",
+ src: "scatter_pivot_2.png",
+ caption: "Pivot point located far down",
+ },
+ ],
+ }),
+ ].join("\n")
+ );
const mesh = Mesh.selected.last();
mesh.unselect();
- const group = new Group({ name: "instances_on_" + mesh.name });
- group.init();
-
const selected = Mesh.selected.slice();
- runEdit(mesh, selected, group, 3);
+ runEdit(mesh, selected, {});
Undo.amendEdit(
{
density: {
type: "number",
value: 3,
- label: "Density",
+ label: "Max Density",
+ min: 0,
+ max: 100,
+ },
+ min_distance: {
+ type: "number",
+ value: 0,
+ label: "Min Distance",
+ min: 0,
+ },
+ scale: {
+ type: "number",
+ value: 100,
+ label: "Scale",
+ min: 0,
+ max: 100,
+ },
+ min_scale: {
+ type: "number",
+ value: 100,
+ label: "Min Scale",
+ min: 0,
+ max: 100,
+ },
+ scale_factor: {
+ type: "number",
+ value: 50,
+ label: "Scale Randomness",
+ min: 0,
+ max: 100,
+ },
+
+ rotation: {
+ type: "number",
+ value: 0,
+ label: "Max Rotation",
+ min: 0,
+ max: 180,
+ },
+ rotation_factor: {
+ type: "number",
+ value: 0,
+ label: "Rotation Randomness",
min: 0,
max: 100,
},
},
(form) => {
- runEdit(mesh, selected, group, form.density, true);
+ runEdit(mesh, selected, form, true);
}
);
});
diff --git a/src/mesh_tools/src/tools/bevel._action.js b/src/mesh_tools/src/tools/bevel._action.js
new file mode 100644
index 00000000..429eb5eb
--- /dev/null
+++ b/src/mesh_tools/src/tools/bevel._action.js
@@ -0,0 +1,750 @@
+/**
+ * !
+ * ! Beveling is really brain draining!!! ...
+ * ! Until my sanes are ready to continue, this action will be left silence.. alone...
+ * !
+ */
+import { action } from "../actions.js";
+import {
+ groupElementsCollided,
+ groupMultipleBy,
+ lerp3,
+} from "../utils/array.js";
+import Neighborhood from "../utils/mesh/neighborhood.js";
+import {
+ extractEdgeKey,
+ getEdgeKey,
+ isEdgeKeySelected,
+ quadrilate,
+ sortVerticesByAngle,
+} from "../utils/utils.js";
+import { distanceBetweenSq } from "../utils/vector.js";
+/**
+ *
+ * @param {*} v1
+ * @param {*} v2
+ * @param {*} factor
+ * @returns {ArrayVector3}
+ */
+function interpolateEdge(v1, v2, factor) {
+ return lerp3(v1, v2, factor);
+ const offset = v2.slice().V3_subtract(v1);
+ const direction = offset.V3_divide(
+ Math.hypot(offset[0], offset[1], offset[2])
+ );
+ return direction.V3_multiply(factor);
+}
+function replaceVertexAndTriangulate(
+ mesh,
+ face,
+ vertexKey,
+ replacementVertices
+) {
+ const vertexKeys = face.getSortedVertices();
+ const index = vertexKeys.indexOf(vertexKey);
+ const nextVertexKey = vertexKeys[(index + 1) % vertexKeys.length];
+ if (
+ replacementVertices.length > 1 &&
+ distanceBetweenSq(
+ mesh.vertices[replacementVertices[0]],
+ mesh.vertices[nextVertexKey]
+ ) <
+ distanceBetweenSq(
+ mesh.vertices[replacementVertices[1]],
+ mesh.vertices[nextVertexKey]
+ )
+ ) {
+ replacementVertices.reverse();
+ }
+
+ vertexKeys.splice(index, 1, ...replacementVertices);
+
+ replaceTriangulated(mesh, face, vertexKeys);
+}
+function runEdit(factor, amend) {
+ Undo.initEdit({ elements: Mesh.selected, selection: true }, amend);
+
+ for (const mesh of Mesh.selected) {
+ let veNeighborhood = Neighborhood.VertexEdges(mesh);
+ let efNeighborhood = Neighborhood.EdgeFaces(mesh);
+ let vfNeighborhood = Neighborhood.VertexFaces(mesh);
+ const beveledEdgeVertices = new Map();
+
+ const oppVertex = (face, edge, vertex) => {
+ return extractEdgeKey(
+ veNeighborhood[vertex].find(
+ (e) =>
+ e != edge && efNeighborhood[e] && efNeighborhood[e].includes(face)
+ )
+ ).find((e) => e != vertex);
+ };
+
+ function insetVertexFaces(
+ vertexKey,
+ edge,
+ newVertexAKey,
+ newVertexBKey,
+ edgesA,
+ edgesB,
+ faces
+ ) {
+ const verticesToInset = veNeighborhood[vertexKey]
+ .filter((e) => e != edge && !edgesA.includes(e) && !edgesB.includes(e))
+ .map(extractEdgeKey)
+ .map((e) => e.find((e) => e != vertexKey));
+
+ const verticesToInsetMap = {};
+ for (const vertexToInset of verticesToInset) {
+ [verticesToInsetMap[vertexToInset]] = mesh.addVertices(
+ interpolateEdge(
+ mesh.vertices[vertexKey],
+ mesh.vertices[vertexToInset],
+ factor
+ )
+ );
+ }
+ const newFaceVertices = Object.values(verticesToInsetMap);
+ newFaceVertices.unshift(newVertexAKey);
+ newFaceVertices.push(newVertexBKey);
+
+ // Inset Face
+ const insetFace = new MeshFace(mesh, {
+ vertices: sortVerticesByAngle(mesh, newFaceVertices),
+ });
+ mesh.addFaces(insetFace);
+ replaceTriangulated(mesh, insetFace);
+
+ for (const face of faces) {
+ const edges = getEdgesOfFace(face);
+ // TODO optimize by making use of face-face neighborhood
+ const isNeighborWithA = edgesA.some((e) => edges.includes(e));
+ const isNeighborWithB = edgesB.some((e) => edges.includes(e));
+
+ const vertexKeys = face.getSortedVertices().slice();
+ const replacementVertices = [];
+
+ if (isNeighborWithA || isNeighborWithB) {
+ replacementVertices.push(
+ isNeighborWithA ? newVertexAKey : newVertexBKey
+ );
+ }
+
+ for (let i = vertexKeys.length - 1; i >= 0; i--) {
+ const vertexKey = vertexKeys[i];
+ if (verticesToInsetMap[vertexKey]) {
+ replacementVertices.push(verticesToInsetMap[vertexKey]);
+ }
+ }
+
+ replaceVertexAndTriangulate(mesh, face, vertexKey, replacementVertices);
+ }
+ }
+ function handleEdgeVertexFaces(
+ vertexKey,
+ edge,
+ newVertexAKey,
+ newVertexBKey,
+ edgesA,
+ edgesB,
+ faces
+ ) {
+ if (faces.length < 1) {
+ return;
+ }
+ if (faces.length > 1) {
+ insetVertexFaces(
+ vertexKey,
+ edge,
+ newVertexAKey,
+ newVertexBKey,
+ edgesA,
+ edgesB,
+ faces
+ );
+ return;
+ }
+
+ for (const face of faces)
+ replaceVertexAndTriangulate(mesh, face, vertexKey, [
+ newVertexBKey,
+ newVertexAKey,
+ ]);
+ }
+
+ for (const vertex in veNeighborhood) {
+ const selectedEdges = veNeighborhood[vertex].filter(
+ isEdgeKeySelected.bind(null, mesh)
+ );
+ const edges = veNeighborhood[vertex];
+ if (selectedEdges.length == 0) continue;
+
+ if (selectedEdges.length == 2) {
+ const edgeA = selectedEdges[0];
+ const edgeB = selectedEdges[1];
+ const vertexA = extractEdgeKey(edgeA).find((e) => e != vertex);
+ const vertexB = extractEdgeKey(edgeB).find((e) => e != vertex);
+
+ const splitterEdges = edges.filter((e) => !selectedEdges.includes(e));
+ function gatherNeighboringFacesUntilEdge(
+ startingEdge,
+ startingFace,
+ targetEdge
+ ) {
+ let currentEdge;
+ const faces = [];
+ const foundEdges = [];
+ const maxIterations = Object.values(mesh.faces).length ** 2;
+ let safetyIndex = 0;
+ while (safetyIndex <= maxIterations && currentEdge != targetEdge) {
+ foundEdges.push(currentEdge ?? startingEdge);
+ const facesEdges = (
+ !currentEdge ? [startingFace] : efNeighborhood[currentEdge]
+ ).map((e) => [e, getEdgesOfFace(e)]);
+
+ currentEdge = undefined;
+ for (const [face, edges] of facesEdges) {
+ const edge = edges.find(
+ (edge) =>
+ !foundEdges.includes(edge) &&
+ (splitterEdges.includes(edge) || edge == targetEdge)
+ );
+ if (edge) {
+ currentEdge = edge;
+ faces.push(face);
+ break;
+ }
+ }
+ if (!currentEdge) break;
+
+ safetyIndex++;
+ }
+
+ return faces;
+ }
+ const facesA = efNeighborhood[edgeA];
+
+ if (facesA.length != 2) {
+ // TODO
+ continue;
+ }
+ const facesB = efNeighborhood[edgeB];
+
+ if (facesB.length != 2) {
+ // TODO
+ continue;
+ }
+
+ /**
+ * Includes facesA[1] and (facesB[0] OR facesB[1])
+ */
+ const splitterFaces = gatherNeighboringFacesUntilEdge(
+ edgeA,
+ facesA[1],
+ edgeB
+ );
+
+ // Make faces in the same order.
+ if (!splitterFaces.includes(facesB[1])) {
+ /**
+ * Now splitterFaces includes facesA[1] and facesB[1]
+ */
+ facesB.reverse();
+ }
+
+ const commonFaces = gatherNeighboringFacesUntilEdge(
+ edgeA,
+ facesA[0],
+ edgeB
+ );
+
+ let vertexCommon;
+ let vertexSplit;
+ {
+ const oppVertexAAKey = oppVertex(facesA[0], edgeA, vertexA);
+ const oppVertexABKey = oppVertex(facesA[1], edgeA, vertexA);
+ const newVertexAA = interpolateEdge(
+ mesh.vertices[vertexA],
+ mesh.vertices[oppVertexAAKey],
+ factor
+ );
+ const newVertexAB = interpolateEdge(
+ mesh.vertices[vertexA],
+ mesh.vertices[oppVertexABKey],
+ factor
+ );
+ const oppVertexBAKey = oppVertex(facesB[0], edgeB, vertexB);
+ const oppVertexBBKey = oppVertex(facesB[1], edgeB, vertexB);
+ const newVertexBA = interpolateEdge(
+ mesh.vertices[vertexB],
+ mesh.vertices[oppVertexBAKey],
+ factor
+ );
+ const newVertexBB = interpolateEdge(
+ mesh.vertices[vertexB],
+ mesh.vertices[oppVertexBBKey],
+ factor
+ );
+ vertexCommon = newVertexAA
+ .V3_subtract(mesh.vertices[vertexA])
+ .V3_add(newVertexBA.V3_subtract(mesh.vertices[vertexB]))
+ .V3_add(mesh.vertices[vertex]);
+ vertexSplit = newVertexAB
+ .V3_subtract(mesh.vertices[vertexA])
+ .V3_add(newVertexBB.V3_subtract(mesh.vertices[vertexB]))
+ .V3_add(mesh.vertices[vertex]);
+ }
+
+ const [vertexCommonKey, vertexSplitKey] = mesh.addVertices(
+ vertexCommon,
+ vertexSplit
+ );
+ /* Handle common */
+ for (const commonFace of commonFaces) {
+ const vertexIndex = commonFace.vertices.indexOf(vertex);
+ commonFace.vertices[vertexIndex] = vertexCommonKey;
+ commonFace.uv[vertexCommonKey] = commonFace.uv[vertex].slice();
+ }
+
+ /* Handle splitter */
+ for (const splitterFace of splitterFaces) {
+ const vertexIndex = splitterFace.vertices.indexOf(vertex);
+ splitterFace.vertices[vertexIndex] = vertexSplitKey;
+ splitterFace.uv[vertexSplitKey] = splitterFace.uv[vertex].slice();
+ }
+
+ /* */
+ beveledEdgeVertices.set(vertex, {
+ newVertexAKey: vertexSplitKey,
+ newVertexBKey: vertexCommonKey,
+ });
+ }
+ if (selectedEdges.length == 1) {
+ const edge = selectedEdges[0];
+ if (efNeighborhood[edge].length < 2) {
+ // TODO Disjoint
+ continue;
+ }
+ if (efNeighborhood[edge].length > 2) {
+ // TODO Non-Manifold
+ continue;
+ }
+
+ const [faceA, faceB] = efNeighborhood[edge];
+ const connectedFaces = vfNeighborhood[vertex].filter(
+ (e) => e != faceA && e != faceB
+ );
+ const edgesA = getEdgesOfFace(faceA);
+ const edgesB = getEdgesOfFace(faceB);
+
+ /* */
+ const oppVertexAKey = oppVertex(faceA, edge, vertex);
+ const oppVertexBKey = oppVertex(faceB, edge, vertex);
+ const newVertexA = interpolateEdge(
+ mesh.vertices[vertex],
+ mesh.vertices[oppVertexAKey],
+ factor
+ );
+ const newVertexB = interpolateEdge(
+ mesh.vertices[vertex],
+ mesh.vertices[oppVertexBKey],
+ factor
+ );
+ let newVertexAKey = mesh.addVertices(newVertexA)[0];
+ let newVertexBKey = mesh.addVertices(newVertexB)[0];
+ handleEdgeVertexFaces(
+ vertex,
+ edge,
+ newVertexAKey,
+ newVertexBKey,
+ edgesA,
+ edgesB,
+ connectedFaces
+ );
+ const vertexAIndex = faceA.vertices.indexOf(vertex);
+ const vertexBIndex = faceB.vertices.indexOf(vertex);
+
+ faceA.vertices[vertexAIndex] = newVertexAKey;
+ faceB.vertices[vertexBIndex] = newVertexBKey;
+
+ faceA.uv[newVertexAKey] = faceA.uv[vertex].slice();
+ faceB.uv[newVertexBKey] = faceB.uv[vertex].slice();
+
+ beveledEdgeVertices.set(vertex, {
+ newVertexAKey,
+ newVertexBKey,
+ });
+ }
+ }
+
+ // TODO use a real array of edges.
+ for (const edge in efNeighborhood) {
+ const [vertex0, vertex1] = extractEdgeKey(edge);
+ if (
+ !beveledEdgeVertices.has(vertex0) ||
+ !beveledEdgeVertices.has(vertex1)
+ )
+ continue;
+ const { newVertexAKey: newVertexA1Key, newVertexBKey: newVertexB1Key } =
+ beveledEdgeVertices.get(vertex1);
+ const { newVertexAKey: newVertexA0Key, newVertexBKey: newVertexB0Key } =
+ beveledEdgeVertices.get(vertex0);
+
+ const newFace = new MeshFace(mesh, {
+ vertices: [
+ newVertexA0Key,
+ newVertexA1Key,
+ newVertexB1Key,
+ newVertexB0Key,
+ ],
+ });
+ UVEditor.setAutoSize(null, true, mesh.addFaces(newFace));
+ }
+ }
+
+ Undo.finishEdit("MTools: Bevel Edges");
+ Canvas.updateView({
+ elements: Mesh.selected,
+ element_aspects: { geometry: true, uv: true, faces: true },
+ selection: true,
+ });
+}
+function runEdit2(factor, amend) {
+ Undo.initEdit({ elements: Mesh.selected, selection: true }, amend);
+
+ for (const mesh of Mesh.selected) {
+ // A district?! 🤔
+ // TODO move into more optimized Neighborhood.District(...);
+ let veNeighborhood;
+ let vfNeighborhood;
+ let efNeighborhood;
+ efNeighborhood = Neighborhood.EdgeFaces(mesh);
+ veNeighborhood = Neighborhood.VertexEdges(mesh);
+ vfNeighborhood = Neighborhood.VertexFaces(mesh);
+
+ const oppVertex = (face, edge, vertex) => {
+ return extractEdgeKey(
+ veNeighborhood[vertex].find(
+ (e) =>
+ e != edge && efNeighborhood[e] && efNeighborhood[e].includes(face)
+ )
+ ).find((e) => e != vertex);
+ };
+
+ const processedEdgeSet = new Set();
+
+ const edges = mesh.getSelectedEdges().map((e) => {
+ e = getEdgeKey(e[0], e[1]);
+ const vertices = extractEdgeKey(e);
+ const tuple = vertices;
+ tuple.push(e);
+
+ return tuple;
+ });
+
+ /**
+ * For Handling 2 connected edges
+ */
+ const verticesToMergeMap = new Map();
+ const verticesToJoinMap = new Map();
+
+ console.log(groupMultipleBy(edges, (value) => value.slice(0, 2)));
+ for (const [vertex0Key, vertex1Key, edge] of edges) {
+ if (processedEdgeSet.has(edge)) continue;
+ processedEdgeSet.add(edge);
+
+ const faces = efNeighborhood[edge];
+ if (faces.length == 0) continue; /* Single Edge */
+ if (faces.length == 1) continue; /* Single Face */
+ if (faces.length > 2) continue; /* Non-Manifold */
+ const [faceA, faceB] = faces;
+
+ const verticesA = faceA.vertices;
+ const verticesB = faceB.vertices;
+
+ const sharedEdges0 = edges.filter(
+ ([v0, v1, e]) => edge != e && (v0 == vertex0Key || v1 == vertex0Key)
+ );
+ const sharedEdges1 = edges.filter(
+ ([v0, v1, e]) => edge != e && (v0 == vertex1Key || v1 == vertex1Key)
+ );
+
+ const oppVertexA0Key = oppVertex(faceA, edge, vertex0Key);
+ const oppVertexA1Key = oppVertex(faceA, edge, vertex1Key);
+ const oppVertexB0Key = oppVertex(faceB, edge, vertex0Key);
+ const oppVertexB1Key = oppVertex(faceB, edge, vertex1Key);
+ const vertexA0Index = verticesA.indexOf(vertex0Key);
+ const vertexA1Index = verticesA.indexOf(vertex1Key);
+ const vertexB0Index = verticesB.indexOf(vertex0Key);
+ const vertexB1Index = verticesB.indexOf(vertex1Key);
+
+ const vertex0 = mesh.vertices[vertex0Key];
+ const vertex1 = mesh.vertices[vertex1Key];
+ const oppVertexA0 = mesh.vertices[oppVertexA0Key];
+ const oppVertexA1 = mesh.vertices[oppVertexA1Key];
+ const oppVertexB0 = mesh.vertices[oppVertexB0Key];
+ const oppVertexB1 = mesh.vertices[oppVertexB1Key];
+
+ const newVertexA0 = lerp3(vertex0, oppVertexA0, factor);
+ const newVertexA1 = lerp3(vertex1, oppVertexA1, factor);
+ const newVertexB0 = lerp3(vertex0, oppVertexB0, factor);
+ const newVertexB1 = lerp3(vertex1, oppVertexB1, factor);
+ const newVertexA0Offset = newVertexA0.slice().V3_subtract(vertex0);
+ const newVertexA1Offset = newVertexA1.slice().V3_subtract(vertex1);
+ const newVertexB0Offset = newVertexB0.slice().V3_subtract(vertex0);
+ const newVertexB1Offset = newVertexB1.slice().V3_subtract(vertex1);
+ /* DO NOT SORT */
+ const A0Edge = [vertex0Key, oppVertexA0Key].join("_");
+ const A1Edge = [vertex1Key, oppVertexA1Key].join("_");
+ const B0Edge = [vertex0Key, oppVertexB0Key].join("_");
+ const B1Edge = [vertex1Key, oppVertexB1Key].join("_");
+ let newVertexA0Key, newVertexA1Key, newVertexB0Key, newVertexB1Key;
+
+ if (sharedEdges0.length == 1) {
+ newVertexA0Key = verticesToMergeMap.get(A0Edge);
+ newVertexB0Key = verticesToMergeMap.get(B0Edge);
+ }
+ if (sharedEdges1.length == 1) {
+ newVertexA1Key = verticesToMergeMap.get(A1Edge);
+ newVertexB1Key = verticesToMergeMap.get(B1Edge);
+ }
+ const toJoin0 = verticesToJoinMap.get(vertex0Key);
+ const toJoin1 = verticesToJoinMap.get(vertex1Key);
+
+ if (toJoin1) {
+ if (!newVertexA1Key) {
+ newVertexA1Key = toJoin1[0];
+ mesh.vertices[newVertexA1Key].V3_add(newVertexA1Offset);
+ }
+ if (!newVertexB1Key) {
+ newVertexB1Key = toJoin1[1];
+ mesh.vertices[newVertexB1Key].V3_add(newVertexB1Offset);
+ }
+ }
+ if (toJoin0) {
+ if (!newVertexA0Key) {
+ newVertexA0Key = toJoin0[0];
+ mesh.vertices[newVertexA0Key].V3_add(newVertexA0Offset);
+ }
+ if (!newVertexB0Key) {
+ newVertexB0Key = toJoin0[1];
+ mesh.vertices[newVertexB0Key].V3_add(newVertexB0Offset);
+ }
+ }
+
+ newVertexA0Key ??= mesh.addVertices(newVertexA0)[0];
+ newVertexB0Key ??= mesh.addVertices(newVertexB0)[0];
+ newVertexA1Key ??= mesh.addVertices(newVertexA1)[0];
+ newVertexB1Key ??= mesh.addVertices(newVertexB1)[0];
+
+ if (sharedEdges0.length == 1) {
+ if (!verticesToJoinMap.has(vertex0Key)) {
+ verticesToJoinMap.set(vertex0Key, [newVertexA0Key, newVertexB0Key]);
+ }
+ }
+ if (sharedEdges1.length == 1) {
+ if (!verticesToJoinMap.has(vertex1Key)) {
+ verticesToJoinMap.set(vertex1Key, [newVertexA1Key, newVertexB1Key]);
+ }
+ }
+
+ verticesToMergeMap.set(A0Edge, newVertexA0Key);
+ verticesToMergeMap.set(B0Edge, newVertexB0Key);
+ verticesToMergeMap.set(A1Edge, newVertexA1Key);
+ verticesToMergeMap.set(B1Edge, newVertexB1Key);
+
+ const newFace = new MeshFace(mesh, {
+ vertices: [
+ newVertexA0Key,
+ newVertexA1Key,
+ newVertexB1Key,
+ newVertexB0Key,
+ ],
+ uv: {
+ [newVertexA0Key]: faceA.uv[vertex0Key].slice(),
+ [newVertexA1Key]: faceA.uv[vertex1Key].slice(),
+ [newVertexB0Key]: faceB.uv[vertex0Key].slice(),
+ [newVertexB1Key]: faceB.uv[vertex1Key].slice(),
+ },
+ });
+ mesh.addFaces(newFace);
+
+ const faces0 = vfNeighborhood[vertex0Key].filter(
+ (e) => e != faceA && e != faceB
+ );
+ const faces1 = vfNeighborhood[vertex1Key].filter(
+ (e) => e != faceA && e != faceB
+ );
+
+ const edgesA = getEdgesOfFace(faceA);
+ const edgesB = getEdgesOfFace(faceB);
+
+ function insetVertexFaces(
+ vertexKey,
+ newVertexAKey,
+ newVertexBKey,
+ faces
+ ) {
+ const verticesToInset = veNeighborhood[vertexKey]
+ .filter(
+ (e) => e != edge && !edgesA.includes(e) && !edgesB.includes(e)
+ )
+ .map(extractEdgeKey)
+ .map((e) => e.find((e) => e != vertexKey));
+
+ const verticesToInsetMap = {};
+ for (const vertexToInset of verticesToInset) {
+ [verticesToInsetMap[vertexToInset]] = mesh.addVertices(
+ lerp3(
+ mesh.vertices[vertexKey],
+ mesh.vertices[vertexToInset],
+ factor
+ )
+ );
+ }
+ const newFaceVertices = Object.values(verticesToInsetMap);
+ newFaceVertices.unshift(newVertexAKey);
+ newFaceVertices.push(newVertexBKey);
+
+ // Inset Face
+ const insetFace = new MeshFace(mesh, { vertices: newFaceVertices });
+ mesh.addFaces(insetFace);
+ replaceTriangulated(mesh, insetFace);
+
+ for (const face of faces) {
+ const edges = getEdgesOfFace(face);
+ // TODO optimize by making use of face-face neighborhood
+ const isNeighborWithA = edgesA.some((e) => edges.includes(e));
+ const isNeighborWithB = edgesB.some((e) => edges.includes(e));
+
+ const vertexKeys = face.getSortedVertices().slice();
+ const replacementVertices = [];
+
+ if (isNeighborWithA || isNeighborWithB) {
+ replacementVertices.push(
+ isNeighborWithA ? newVertexAKey : newVertexBKey
+ );
+ }
+
+ for (let i = vertexKeys.length - 1; i >= 0; i--) {
+ const vertexKey = vertexKeys[i];
+ if (verticesToInsetMap[vertexKey]) {
+ replacementVertices.push(verticesToInsetMap[vertexKey]);
+ }
+ }
+
+ replaceVertexAndTriangulate(
+ mesh,
+ face,
+ vertexKey,
+ replacementVertices
+ );
+ }
+ }
+ function handleEdgeVertexFaces(
+ vertexKey,
+ newVertexAKey,
+ newVertexBKey,
+ faces
+ ) {
+ if (faces.length < 1) {
+ return;
+ }
+
+ if (faces.length > 1) {
+ insetVertexFaces(vertexKey, newVertexAKey, newVertexBKey, faces);
+ return;
+ }
+
+ for (const face of faces)
+ replaceVertexAndTriangulate(mesh, face, vertexKey, [
+ newVertexBKey,
+ newVertexAKey,
+ ]);
+ }
+
+ if (sharedEdges0.length == 0) {
+ handleEdgeVertexFaces(
+ vertex0Key,
+ newVertexA0Key,
+ newVertexB0Key,
+ faces0
+ );
+ }
+ if (sharedEdges1.length == 0) {
+ handleEdgeVertexFaces(
+ vertex1Key,
+ newVertexA1Key,
+ newVertexB1Key,
+ faces1
+ );
+ }
+
+ faceA.vertices[vertexA0Index] = newVertexA0Key;
+ faceA.vertices[vertexA1Index] = newVertexA1Key;
+ faceB.vertices[vertexB0Index] = newVertexB0Key;
+ faceB.vertices[vertexB1Index] = newVertexB1Key;
+
+ faceA.uv[newVertexA0Key] = faceA.uv[vertex0Key].slice();
+ faceA.uv[newVertexA1Key] = faceA.uv[vertex1Key].slice();
+ faceB.uv[newVertexB0Key] = faceB.uv[vertex0Key].slice();
+ faceB.uv[newVertexB1Key] = faceB.uv[vertex1Key].slice();
+ }
+ }
+
+ Undo.finishEdit("MTools: Bevel Edges");
+ Canvas.updateView({
+ elements: Mesh.selected,
+ element_aspects: { geometry: true, uv: true, faces: true },
+ selection: true,
+ });
+}
+export default action("bevel", () => {
+ runEdit(0.1, false);
+ Undo.amendEdit(
+ {
+ factor: {
+ label: "Width Percentage",
+ type: "number",
+ value: 10,
+ min: 0,
+ max: 100,
+ },
+ },
+ ({ factor }) => runEdit(factor / 100, true)
+ );
+});
+function getEdgesOfFace(face) {
+ return groupElementsCollided(face.getSortedVertices(), 2).map((e) =>
+ getEdgeKey(e[0], e[1])
+ );
+}
+function fillInAbsentUVS(face) {
+ for (const vertex of face.vertices) {
+ face.uv[vertex] ??= [0, 0]; // TODO
+ }
+ if (
+ face.vertices.map((e) => face.uv[e]).allAre((e) => e[0] == 0 && e[1] == 0)
+ ) {
+ UVEditor.setAutoSize(null, true, [face.getFaceKey()]);
+ }
+}
+function replaceTriangulated(
+ mesh,
+ face,
+ vertexKeys = face.getSortedVertices()
+) {
+ fillInAbsentUVS(face);
+ if (vertexKeys.length == 4) return;
+
+ const vertices = vertexKeys.map((e) => mesh.vertices[e]);
+ const quadrilaterals = quadrilate(vertices);
+
+ delete mesh.faces[face.getFaceKey()];
+
+ for (const quadrilateral of quadrilaterals) {
+ const newFace = new MeshFace(mesh, face).extend({
+ vertices: quadrilateral.map((e) => vertexKeys[e]),
+ });
+ mesh.addFaces(newFace);
+ }
+}
diff --git a/src/mesh_tools/src/tools/bridge_edge_loops.action.js b/src/mesh_tools/src/tools/bridge_edge_loops.action.js
index bf1c5570..c5964749 100644
--- a/src/mesh_tools/src/tools/bridge_edge_loops.action.js
+++ b/src/mesh_tools/src/tools/bridge_edge_loops.action.js
@@ -7,18 +7,20 @@
*/
import { action } from "../actions.js";
+import {
+ findMin,
+ groupElementsCollided,
+ lerp3,
+ offsetArray,
+} from "../utils/array.js";
import {
CubicBezier as CB,
closestToLine,
computeCentroid,
computeTriangleNormal,
extractEdgeKey,
- findMin,
getSelectedEdgesConnectedCountMap,
getSelectedFacesAndEdgesByVertices,
- groupElementsCollided,
- lerp3,
- offsetArray,
selectFacesAndEdgesByVertices,
} from "../utils/utils.js";
import {
@@ -116,7 +118,7 @@ function bridgeLoopsConfigured(
edgeLoopB,
centroidA,
centroidB,
- { twist, numberOfCuts, blendPath, blendInfluence }
+ { twist, numberOfCuts, blendPath, blendInfluence, reverse }
) {
if (edgeLoopA.length < 3 || edgeLoopB.length < 3) {
return;
@@ -124,24 +126,20 @@ function bridgeLoopsConfigured(
edgeLoopA = edgeLoopA.map((e) => e.slice());
edgeLoopB = edgeLoopB.map((e) => e.slice());
- const bestOffset = bestEdgeLoopsOffset(edgeLoopB, edgeLoopA, mesh);
+ const bestOffset = bestEdgeLoopsOffset(edgeLoopA, edgeLoopB, mesh);
offsetArray(edgeLoopB, bestOffset);
const reversedEdgeLoopB = edgeLoopB.map((e) => e.slice().reverse()).reverse();
const bestOffsetReversed = bestEdgeLoopsOffset(
- reversedEdgeLoopB,
edgeLoopA,
+ reversedEdgeLoopB,
mesh
);
- // Negation of `bestOffset2` since the array is reversed,
- // Does it make ANY sense?
- // It doesn't!
- // It just happens to work.
- offsetArray(reversedEdgeLoopB, -bestOffsetReversed);
+ offsetArray(reversedEdgeLoopB, bestOffsetReversed);
if (
- edgeLoopsLength(mesh, edgeLoopA, edgeLoopB) >
- edgeLoopsLength(mesh, edgeLoopA, reversedEdgeLoopB)
+ edgeLoopsLength(mesh, edgeLoopA, reverse ? edgeLoopB : reversedEdgeLoopB) <
+ edgeLoopsLength(mesh, edgeLoopA, reverse ? reversedEdgeLoopB : edgeLoopB)
) {
edgeLoopB = reversedEdgeLoopB;
}
@@ -268,6 +266,7 @@ function runEdit(
cutHoles,
blendPath,
blendInfluence,
+ reverse
) {
Undo.initEdit({ elements: Mesh.selected, selection: true }, amend);
@@ -370,7 +369,11 @@ function runEdit(
};
}
- const sortedEdgeLoops = [loops.pop()];
+ const furthestLoop = findMin(loops, e => e.centroid.length());
+ loops.remove(furthestLoop);
+
+ const sortedEdgeLoops = [furthestLoop];
+ mesh.addVertices(sortedEdgeLoops[0].centroid.toArray())
while (loops.length) {
const currEdgeLoop = sortedEdgeLoops.last();
const closestLoop = findMin(loops, (e) =>
@@ -396,6 +399,7 @@ function runEdit(
numberOfCuts,
blendPath,
blendInfluence,
+ reverse
}
);
}
@@ -411,11 +415,11 @@ export default action("bridge_edge_loops", () => {
Undo.amendEdit(
{
- blend_path: {
- type: "checkbox",
- label: "Blend Path",
- value: true,
- },
+ // reverse: {
+ // type: "checkbox",
+ // label: "Reverse Winding",
+ // value: false,
+ // },
blend_influence: {
type: "number",
label: "Smoothness",
@@ -434,6 +438,11 @@ export default action("bridge_edge_loops", () => {
label: "Twist",
value: 0,
},
+ blend_path: {
+ type: "checkbox",
+ label: "Blend Path",
+ value: true,
+ },
cut_holes: {
type: "checkbox",
label: "Cut Holes",
@@ -449,6 +458,7 @@ export default action("bridge_edge_loops", () => {
form.cut_holes,
form.blend_path,
form.blend_influence / 100,
+ form.reverse
);
}
);
diff --git a/src/mesh_tools/src/tools/expand_selection.action.js b/src/mesh_tools/src/tools/expand_selection.action.js
index 9d4ab7b6..96f89d8f 100644
--- a/src/mesh_tools/src/tools/expand_selection.action.js
+++ b/src/mesh_tools/src/tools/expand_selection.action.js
@@ -1,9 +1,10 @@
import { action } from "../actions.js";
-import { computeVertexNeighborhood, selectFacesAndEdgesByVertices } from "../utils/utils.js";
+import Neighborhood from "../utils/mesh/neighborhood.js";
+import { selectFacesAndEdgesByVertices } from "../utils/utils.js";
export default action("expand_selection", () => {
Mesh.selected.forEach((mesh) => {
- const neighborMap = computeVertexNeighborhood(mesh);
+ const neighborMap = Neighborhood.VertexVertices(mesh);
const selectedVertices = mesh.getSelectedVertices();
const selectedVertexSet = new Set(selectedVertices);
diff --git a/src/mesh_tools/src/tools/laplacian_smooth.action.js b/src/mesh_tools/src/tools/laplacian_smooth.action.js
index e20aba5f..077bd1a8 100644
--- a/src/mesh_tools/src/tools/laplacian_smooth.action.js
+++ b/src/mesh_tools/src/tools/laplacian_smooth.action.js
@@ -1,5 +1,5 @@
import { action } from "../actions.js";
-import { computeVertexNeighborhood } from "../utils/utils.js";
+import Neighborhood from "../utils/mesh/neighborhood.js";
function runEdit(amend = false, influence = 1, iterations = 1) {
Undo.initEdit({ elements: Mesh.selected, selection: true }, amend);
@@ -8,7 +8,7 @@ function runEdit(amend = false, influence = 1, iterations = 1) {
if (!influence || !iterations) return; //
const { vertices } = mesh;
- const neighborMap = computeVertexNeighborhood(mesh);
+ const neighborMap = Neighborhood.VertexVertices(mesh);
const selectedVertices = mesh.getSelectedVertices();
diff --git a/src/mesh_tools/src/tools/shrink_selection.action.js b/src/mesh_tools/src/tools/shrink_selection.action.js
index c9556669..3ac963c6 100644
--- a/src/mesh_tools/src/tools/shrink_selection.action.js
+++ b/src/mesh_tools/src/tools/shrink_selection.action.js
@@ -1,9 +1,10 @@
import { action } from "../actions.js";
-import { computeVertexNeighborhood, selectFacesAndEdgesByVertices } from "../utils/utils.js";
+import Neighborhood from "../utils/mesh/neighborhood.js";
+import { selectFacesAndEdgesByVertices } from "../utils/utils.js";
export default action("shrink_selection", () => {
Mesh.selected.forEach((mesh) => {
- const neighborMap = computeVertexNeighborhood(mesh);
+ const neighborMap = Neighborhood.VertexVertices(mesh);
const selectedVertices = mesh.getSelectedVertices();
const selectedVertexSet = new Set(selectedVertices);
diff --git a/src/mesh_tools/src/tools/tris_to_quad.action.js b/src/mesh_tools/src/tools/tris_to_quad.action.js
index 53acd529..04ca8c09 100644
--- a/src/mesh_tools/src/tools/tris_to_quad.action.js
+++ b/src/mesh_tools/src/tools/tris_to_quad.action.js
@@ -1,5 +1,6 @@
import { action } from "../actions.js";
-import { areVectorsCollinear, isValidQuad, minIndex } from "../utils/utils.js";
+import { minIndex } from "../utils/array.js";
+import { isValidQuad } from "../utils/utils.js";
function runEdit(maxAngle, ignoreDisjointUVs = true, amend = false) {
Undo.initEdit({ elements: Mesh.selected, selection: true }, amend);
diff --git a/src/mesh_tools/src/utils/array.js b/src/mesh_tools/src/utils/array.js
new file mode 100644
index 00000000..e43cd0e7
--- /dev/null
+++ b/src/mesh_tools/src/utils/array.js
@@ -0,0 +1,122 @@
+/**
+ * @template {V}
+ * @template {K}
+ * @param {V[]} arr
+ * @param {(value: V, currentIndex: number, array: V[]) => K[]} callback
+ * @returns {{[k: K]: V[]}}
+ */
+export function groupMultipleBy(arr, callback) {
+ return arr.reduce((acc, ...args) => {
+ const keys = callback(...args);
+ for (const key of keys) {
+ acc[key] ??= [];
+ acc[key].push(args[0]);
+ }
+ return acc;
+ }, {});
+}
+
+export function minIndex(array) {
+ let minI = -1;
+ let minValue = Infinity;
+ for (let i = 0; i < array.length; i++) {
+ const value = array[i];
+
+ if (value <= minValue) {
+ minValue = value;
+ minI = i;
+ }
+ }
+ return minI;
+}
+export function findMin(array, map = (x) => x) {
+ if (array.length == 1) return array[0];
+ if (array.length == 0) return null;
+
+ let minElement = null;
+ let minValue = Infinity;
+
+ for (const element of array) {
+ const value = map(element);
+
+ if (value <= minValue) {
+ minElement = element;
+ minValue = value;
+ }
+ }
+
+ return minElement;
+}
+
+/**
+ *
+ * @param {ArrayVector3} a
+ * @param {ArrayVector3} b
+ * @param {number} t
+ * @returns {ArrayVector3}
+ */
+export function lerp3(a, b, t) {
+ return a.map((e, i) => Math.lerp(e, b[i], t));
+}
+export function groupElementsCollided(array, every = 2) {
+ const newArray = [];
+ for (let i = 0; i < array.length; i++) {
+ const sub = [];
+ for (let j = 0; j < every; j++) {
+ const element = array[(i + j) % array.length];
+ sub.push(element);
+ }
+ newArray.push(sub);
+ }
+ return newArray;
+}
+
+export function offsetArray(array, offset) {
+ while (offset < 0) offset += array.length;
+ while (offset >= array.length) offset -= array.length;
+
+ const newArr = [];
+ for (let i = 0; i < array.length; i++) {
+ newArr[(i + offset) % array.length] = array[i];
+ }
+
+ array.splice(0, Infinity, ...newArr);
+}
+export function deepIncludes(array, value) {
+ for (const item of array) {
+ if (item === value) {
+ return true;
+ }
+ if (item instanceof Array && deepIncludes(item, value)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * @template {T}
+ * @param {T[]} array
+ * @returns {T[]}
+ */
+export function distinguishArray(array) {
+ const distinctArray = [];
+ for (const element of array) {
+ if (distinctArray.includes(element)) continue;
+
+ distinctArray.push(element);
+ }
+ return distinctArray;
+}
+
+export function distinctlyMergeArrays(...arrays) {
+ const distinctArray = [];
+ for (const array of arrays) {
+ for (const element of array) {
+ if (distinctArray.includes(element)) continue;
+
+ distinctArray.push(element);
+ }
+ }
+ return distinctArray;
+}
diff --git a/src/mesh_tools/src/utils/docs.js b/src/mesh_tools/src/utils/docs.js
new file mode 100644
index 00000000..b4292fe7
--- /dev/null
+++ b/src/mesh_tools/src/utils/docs.js
@@ -0,0 +1,69 @@
+const LanguageDefinitions = {
+ "word.before": "Input",
+ "word.after": "Result",
+ "word.mesh": "Mesh",
+ "word.uv": "UV",
+};
+export function getLanguage() {
+ return LanguageDefinitions;
+}
+export function translate(subject) {
+ return subject.replace(/[a-zA-Z_][a-zA-Z0-9_\.]+/g, (key) => {
+ return getLanguage(LanguageDefinitions)[key] ?? key;
+ });
+
+}
+const getURL = (e) =>
+ // `http://127.0.0.1:5500/${e}?t=${Math.random()}`;
+ `https://github.com/Malik12tree/blockbench-plugins/blob/master/src/mesh_tools/${e}?raw=true`;
+
+export function renderPill(title) {
+ return `${title.toString().toUpperCase()}`;
+}
+export function renderImage({ src, caption = "" }) {
+ return `
+
+ `;
+}
+export function renderOverflow(children) {
+ return `${children}`;
+}
+export function renderInsetRow({ items }) {
+ return `
+ ${items
+ .map(
+ (e) =>
+ `
${renderLine(
+ e
+ )}
`
+ )
+ .join("\n")}
+
+ `;
+}
+export function renderLine(options) {
+ if (typeof options == "string") return options;
+
+ switch (options.type) {
+ case "image":
+ return renderImage(options);
+ case "overflow":
+ return renderOverflow(options);
+ case "inset_row":
+ return renderInsetRow(options);
+ default:
+ throw new Error(`Unknown line type: ${options.type}`);
+ }
+}
diff --git a/src/mesh_tools/src/utils/info.js b/src/mesh_tools/src/utils/info.js
new file mode 100644
index 00000000..1ffee950
--- /dev/null
+++ b/src/mesh_tools/src/utils/info.js
@@ -0,0 +1,37 @@
+import { storage } from "../globals.js";
+
+const dontShowAgainInfoStorage = storage.in("dont_show_again_info_storage");
+export function dontShowAgainInfo(id, title, message) {
+ if (dontShowAgainInfoStorage.has(id)) {
+ return;
+ }
+
+ const messageBox = Blockbench.showMessageBox(
+ {
+ title,
+ message,
+ icon: "info",
+ checkboxes: {
+ dont_show_again: { value: false, text: "dialog.dontshowagain" },
+ },
+ buttons: ["dialog.ok"],
+ },
+ (_, { dont_show_again: dontShowAgain }) => {
+ if (dontShowAgain) {
+ dontShowAgainInfoStorage.set(id, true);
+ }
+ }
+ );
+ messageBox.object.querySelector(".dialog_content").style.overflow = "auto";
+}
+
+/**
+ *
+ * @param {string} message
+ * @param {?number} timeout
+ * @returns {never}
+ */
+export function throwQuickMessage(message, timeout) {
+ Blockbench.showQuickMessage(message, timeout);
+ throw message;
+}
diff --git a/src/mesh_tools/src/utils/mesh/neighborhood.js b/src/mesh_tools/src/utils/mesh/neighborhood.js
new file mode 100644
index 00000000..fae7647b
--- /dev/null
+++ b/src/mesh_tools/src/utils/mesh/neighborhood.js
@@ -0,0 +1,96 @@
+import { getEdgeKey } from "../utils.js";
+
+export default class Neighborhood {
+ /**
+ *
+ * @param {Mesh} mesh
+ * @returns {{[vertexKey: string]: string[]}}
+ */
+ static VertexVertices(mesh) {
+ const map = {};
+
+ for (const key in mesh.faces) {
+ const face = mesh.faces[key];
+
+ face.vertices.forEach((vkey) => {
+ if (!(vkey in map)) {
+ map[vkey] = [];
+ }
+
+ face.vertices.forEach((neighborkey) => {
+ if (neighborkey == vkey) return;
+
+ map[vkey].safePush(neighborkey);
+ });
+ });
+ }
+
+ return map;
+ }
+
+ /**
+ *
+ * @param {Mesh} mesh
+ * @returns {{[vertexKey: string]: MeshFace[]}}
+ */
+ static VertexFaces(mesh) {
+ const neighborhood = {};
+
+ for (const key in mesh.faces) {
+ const face = mesh.faces[key];
+
+ for (const vertexKey of face.vertices) {
+ neighborhood[vertexKey] ??= [];
+ neighborhood[vertexKey].safePush(face);
+ }
+ }
+
+ return neighborhood;
+ }
+
+ /**
+ *
+ * @param {Mesh} mesh
+ * @returns {{[edgeKey: string]: MeshFace[]}}
+ */
+ static EdgeFaces(mesh) {
+ const neighborhood = {};
+ for (const key in mesh.faces) {
+ const face = mesh.faces[key];
+ const vertices = face.getSortedVertices();
+
+ for (let i = 0; i < vertices.length; i++) {
+ const vertexCurr = vertices[i];
+ const vertexNext = vertices[(i + 1) % vertices.length];
+ const edgeKey = getEdgeKey(vertexCurr, vertexNext);
+ neighborhood[edgeKey] ??= [];
+ neighborhood[edgeKey].safePush(face);
+ }
+ }
+ return neighborhood;
+ }
+
+ /**
+ *
+ * @param {Mesh} mesh
+ * @returns {{[vertexKey: string]: string[]}}
+ */
+ static VertexEdges(mesh) {
+ const neighborhood = {};
+ for (const key in mesh.faces) {
+ const face = mesh.faces[key];
+ const vertices = face.getSortedVertices();
+
+ for (let i = 0; i < vertices.length; i++) {
+ const vertexCurr = vertices[i];
+ const vertexNext = vertices[(i + 1) % vertices.length];
+ const edgeKey = getEdgeKey(vertexCurr, vertexNext);
+ neighborhood[vertexCurr] ??= [];
+ neighborhood[vertexNext] ??= [];
+ neighborhood[vertexCurr].safePush(edgeKey);
+ neighborhood[vertexNext].safePush(edgeKey);
+ }
+ }
+ return neighborhood;
+ }
+}
diff --git a/src/mesh_tools/src/utils/storage.js b/src/mesh_tools/src/utils/storage.js
new file mode 100644
index 00000000..f3196865
--- /dev/null
+++ b/src/mesh_tools/src/utils/storage.js
@@ -0,0 +1,109 @@
+const KEYS_KEY = "";
+const SUBS_KEY = "";
+class BasicQualifiedStorage {
+ constructor(id) {
+ this.id = id;
+ }
+ #isQualified() {
+ return this.id.startsWith("@");
+ }
+ qualifyKey(key) {
+ if (this.#isQualified()) {
+ return `${this.id}/${key}`;
+ }
+ return `@${this.id}/${key}`;
+ }
+ set(key, value) {
+ key = this.qualifyKey(key);
+
+ localStorage.setItem(key, JSON.stringify(value));
+ }
+ delete(key) {
+ key = this.qualifyKey(key);
+
+ localStorage.removeItem(key);
+ }
+ has(key) {
+ key = this.qualifyKey(key);
+
+ return localStorage.hasOwnProperty(key);
+ }
+ get(key) {
+ key = this.qualifyKey(key);
+
+ const rawValue = localStorage.getItem(key);
+ if (rawValue != null) {
+ return JSON.parse(rawValue);
+ }
+ return null;
+ }
+ update(key, callback, defaultValue) {
+ const value = this.get(key) ?? defaultValue;
+ const newValue = callback(value);
+ return this.set(key, newValue);
+ }
+}
+
+const keysStorage = new BasicQualifiedStorage(KEYS_KEY);
+const subStoragesStorage = new BasicQualifiedStorage(SUBS_KEY);
+export class QualifiedStorage extends BasicQualifiedStorage {
+
+ in(key) {
+ subStoragesStorage.update(this.id, (keys) => {
+ keys.safePush(key);
+ return keys;
+ }, []);
+ return new QualifiedStorage(this.qualifyKey(key));
+ }
+
+ constructor(id) {
+ console.assert(
+ id != KEYS_KEY,
+ `QualifiedStorage: id cannot be equal to ${JSON.stringify(KEYS_KEY)}`
+ );
+
+ super(id);
+ }
+ set(key, value) {
+ keysStorage.update(
+ this.id,
+ (keys) => {
+ keys.safePush(key);
+ return keys;
+ },
+ []
+ );
+
+ super.set(key, value);
+ }
+ delete(key) {
+ keysStorage.update(
+ this.id,
+ (keys) => {
+ const index = keys.indexOf(key);
+ if (index != -1) {
+ keys.splice(index, 1);
+ }
+
+ return keys;
+ },
+ []
+ );
+
+ super.delete(key);
+ }
+ getAllKeys() {
+ return keysStorage.get(this.id) ?? [];
+ }
+ clear() {
+ for (const key of this.getAllKeys()) {
+ super.delete(key);
+ }
+ const subKeys = subStoragesStorage.get(this.id) ?? [];
+ for (const subKey of subKeys) {
+ new QualifiedStorage(this.qualifyKey(subKey)).clear();
+ }
+ keysStorage.delete(this.id);
+ subStoragesStorage.delete(this.id);
+ }
+}
diff --git a/src/mesh_tools/src/utils/utils.js b/src/mesh_tools/src/utils/utils.js
index 5690c538..352c99a4 100644
--- a/src/mesh_tools/src/utils/utils.js
+++ b/src/mesh_tools/src/utils/utils.js
@@ -1,3 +1,5 @@
+import { groupElementsCollided } from "./array.js";
+import Neighborhood from "./mesh/neighborhood.js";
import { addVectors, getX, getY, getZ, isZeroVector } from "./vector.js";
const reusableEuler1 = new THREE.Euler();
@@ -21,9 +23,16 @@ for (let x = 0; x < 256; x++) gradient256[[x, 0]] = x / 255;
*/
const reusableObject = new THREE.Object3D();
reusableObject.rotation.order = "XYZ";
-export function rotationFromDirection(target, targetEuler = new THREE.Euler()) {
+export function rotationFromDirection(
+ target,
+ targetEuler = new THREE.Euler(),
+ { rotateX = 0, rotateY = 0, rotateZ = 0 } = {}
+) {
reusableObject.lookAt(target);
reusableObject.rotateX(Math.degToRad(90));
+ reusableObject.rotateX(rotateX);
+ reusableObject.rotateY(rotateY);
+ reusableObject.rotateZ(rotateZ);
targetEuler.copy(reusableObject.rotation);
return targetEuler;
@@ -85,29 +94,6 @@ export function easeInOutSine(x) {
return -(Math.cos(Math.PI * x) - 1) / 2;
}
-/** @param {Mesh} mesh */
-export function computeVertexNeighborhood(mesh) {
- const map = {};
-
- for (const key in mesh.faces) {
- const face = mesh.faces[key];
-
- face.vertices.forEach((vkey) => {
- if (!(vkey in map)) {
- map[vkey] = [];
- }
-
- face.vertices.forEach((neighborkey) => {
- if (neighborkey == vkey) return;
-
- map[vkey].safePush(neighborkey);
- });
- });
- }
-
- return map;
-}
-
export function getAdjacentElements(arr, index) {
return [
arr[(index + 1 + arr.length) % arr.length],
@@ -233,8 +219,7 @@ export function projectOnPlane(polygonOrPoint, plane) {
* Triangulates a polygon into a set of triangles.
*
* @param {ArrayVector3[]} polygon
- * @returns {Array} An array of triangles. Each triangle is represented
- * as an ArrayVector3
+ * @returns {Array} An array of triangles.
*/
export function triangulate(polygon) {
const vertices3d = polygon.map((v) => v.V3_toThree());
@@ -282,6 +267,67 @@ export function triangulate(polygon) {
return triangles;
}
+/**
+ * @template T
+ * @param {[T, T, T]} triangleA
+ * @param {[T, T, T]} triangleB
+ * @returns {boolean}
+ */
+function areTrianglesAdjacent(triangleA, triangleB) {
+ let sharedCount = 0;
+ for (const a of triangleA) {
+ sharedCount += triangleB.includes(a);
+ if (sharedCount == 2) break;
+ }
+ return sharedCount == 2;
+}
+/**
+ * Quadrilates a polygon into a set of quadrilaterals.
+ *
+ * @param {ArrayVector3[]} polygon
+ * @returns {Array} An array of quadrilate/triangles.
+ */
+export function quadrilate(polygon) {
+ const triangles = triangulate(polygon);
+ const processedTriangles = new Set();
+
+ const quadrilaterals = [];
+ for (const triangle of triangles) {
+ if (processedTriangles.has(triangle)) continue;
+ const adjacentTriangle = triangles.find(
+ (e) => areTrianglesAdjacent(triangle, e) && !processedTriangles.has(e)
+ );
+
+ if (!adjacentTriangle) continue;
+ const sharedVertices = adjacentTriangle.filter((e) => triangle.includes(e));
+ const uniqueVertex = adjacentTriangle.find(
+ (e) => !sharedVertices.includes(e)
+ );
+ if (!uniqueVertex) continue;
+ if (sharedVertices.length != 2) continue;
+
+ // TODO optimize
+ const triangleEdges = groupElementsCollided(triangle);
+ const index = triangleEdges.findIndex(
+ (e) => !e.some((f) => !sharedVertices.includes(f))
+ );
+ if (index == -1) continue;
+ processedTriangles.add(adjacentTriangle);
+ processedTriangles.add(triangle);
+
+ const quadrilateral = triangle; /* .slice() */
+ quadrilateral.splice(index, 0, uniqueVertex);
+
+ quadrilaterals.push(quadrilateral);
+ }
+ for (const triangle of triangles) {
+ if (!processedTriangles.has(triangle)) {
+ quadrilaterals.push(triangle);
+ }
+ }
+
+ return quadrilaterals;
+}
export function worldToScreen(p, camera, width, height) {
// https://stackoverflow.com/a/27448966/16079500
@@ -323,20 +369,6 @@ export function snakeToPascal(subject) {
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(" ");
}
-
-export function minIndex(array) {
- let minI = -1;
- let minValue = Infinity;
- for (let i = 0; i < array.length; i++) {
- const value = array[i];
-
- if (value <= minValue) {
- minValue = value;
- minI = i;
- }
- }
- return minI;
-}
/**
*
* @param {ArrayVector3[]} points
@@ -458,7 +490,7 @@ function gatherConnectedVertices(
);
}
if (!neighborhood) {
- neighborhood = computeVertexNeighborhood(mesh);
+ neighborhood = Neighborhood.VertexVertices(mesh);
}
const connected = new Set([vertex]);
@@ -479,7 +511,7 @@ function gatherConnectedVertices(
}
return connected;
}
-function sortVerticesByAngle(mesh, vertexKeys) {
+export function sortVerticesByAngle(mesh, vertexKeys) {
const vertices = vertexKeys.map((e) => mesh.vertices[e].V3_toThree());
const vertices2d = projectIntoOwnPlane(vertices);
@@ -512,6 +544,19 @@ export function getEdgeKey(a, b) {
}
return `${a}_${b}`;
}
+export function sortEdgeVertices(arr) {
+ return arr.sort();
+}
+export function isEdgeKeySelected(mesh, edge) {
+ const edges = mesh.getSelectedEdges();
+ // TODO optimize
+ return edges.map(([a, b]) => getEdgeKey(a, b)).includes(edge);
+}
+/**
+ *
+ * @param {string} edgeKey
+ * @returns {[string, string]}
+ */
export function extractEdgeKey(edgeKey) {
return edgeKey.split("_");
}
@@ -523,7 +568,7 @@ export function getSelectedEdgesConnectedCountMap(mesh) {
const selectedConnectedCount = {};
const connectedCount = {};
- const neighborhood = computeEdgeFacesNeighborhood(mesh);
+ const neighborhood = Neighborhood.EdgeFaces(mesh);
for (const [a, b] of edges) {
const edgeKey = getEdgeKey(a, b);
@@ -541,27 +586,7 @@ export function getSelectedEdgesConnectedCountMap(mesh) {
}
return { connectedCount, selectedConnectedCount };
}
-/**
- *
- * @param {Mesh} mesh
- * @returns {{[edgeKey: string]: MeshFace[]}}
- */
-export function computeEdgeFacesNeighborhood(mesh) {
- const neighborhood = {};
- for (const key in mesh.faces) {
- const face = mesh.faces[key];
- const vertices = face.getSortedVertices();
-
- for (let i = 0; i < vertices.length; i++) {
- const vertexCurr = vertices[i];
- const vertexNext = vertices[(i + 1) % vertices.length];
- const edgeKey = getEdgeKey(vertexCurr, vertexNext);
- neighborhood[edgeKey] ??= [];
- neighborhood[edgeKey].safePush(face);
- }
- }
- return neighborhood;
-}
+
/**
* @param {Mesh} mesh
*/
@@ -583,7 +608,7 @@ export function groupLoopsIncluding(mesh, verticesSet) {
return loops;
}
export function groupConnectedVerticesIncluding(mesh, verticesSet) {
- const neighborhood = computeVertexNeighborhood(mesh);
+ const neighborhood = Neighborhood.VertexVertices(mesh);
const processedVertices = new Set();
const groups = [];
@@ -668,45 +693,6 @@ export function vertexNormal(mesh, vertexKey) {
return avgNormal;
}
-/**
- *
- * @param {ArrayVector3} a
- * @param {ArrayVector3} b
- * @param {number} t
- * @returns {ArrayVector3}
- */
-export function lerp3(a, b, t) {
- return a.map((e, i) => Math.lerp(e, b[i], t));
-}
-
-export function groupElementsCollided(array, every = 2) {
- const newArray = [];
- for (let i = 0; i < array.length; i++) {
- const sub = [];
- for (let j = 0; j < every; j++) {
- const element = array[(i + j) % array.length];
- sub.push(element);
- }
- newArray.push(sub);
- }
- return newArray;
-}
-
-export function findMin(array, map = (x) => x) {
- let minElement = null;
- let minValue = Infinity;
-
- for (const element of array) {
- const value = map(element);
-
- if (value <= minValue) {
- minElement = element;
- minValue = value;
- }
- }
-
- return minElement;
-}
export function computeCentroid(polygon) {
const centroid = new THREE.Vector3();
for (const vertex of polygon) {
@@ -716,17 +702,6 @@ export function computeCentroid(polygon) {
return centroid;
}
-export function offsetArray(array, offset) {
- while (offset < 0) offset += array.length;
- while (offset >= array.length) offset -= array.length;
-
- const newArr = [];
- for (let i = 0; i < array.length; i++) {
- newArr[(i + offset) % array.length] = array[i];
- }
-
- array.splice(0, Infinity, ...newArr);
-}
/**
*
@@ -784,14 +759,4 @@ export function CubicBezierTangent(t, p0, p1, p2, p3) {
6 * (1 - t) * t * (p2 - p1) +
3 * t ** 2 * (p3 - p2)
);
-}
-/**
- *
- * @param {string} message
- * @param {?number} timeout
- * @returns {never}
- */
-export function throwQuickMessage(message, timeout) {
- Blockbench.showQuickMessage(message, timeout);
- throw message;
-}
+}
\ No newline at end of file
diff --git a/src/mesh_tools/src/utils/vector.js b/src/mesh_tools/src/utils/vector.js
index 3893f8bb..4b2991d3 100644
--- a/src/mesh_tools/src/utils/vector.js
+++ b/src/mesh_tools/src/utils/vector.js
@@ -84,6 +84,17 @@ export function addedVectors(a, b, three = true) {
export function distanceBetween(a, b) {
return Math.hypot(getX(a) - getX(b), getY(a) - getY(b), getZ(a) - getZ(b));
}
+/**
+ * @param {Vector3} a
+ * @param {Vector3} b
+ */
+export function distanceBetweenSq(a, b) {
+ return (
+ (getX(a) - getX(b)) ** 2 +
+ (getY(a) - getY(b)) ** 2 +
+ (getZ(a) - getZ(b)) ** 2
+ );
+}
/**
* @param {Vector3} vector
*/