diff --git a/plugins.json b/plugins.json index 753a147b..2682df98 100644 --- a/plugins.json +++ b/plugins.json @@ -584,10 +584,11 @@ "author": "Malik12tree", "icon": "icon.png", "description": "Adds powerful mesh modeling tools, operators and generators!", - "version": "2.0.0", + "version": "2.0.1", "min_version": "4.9.4", "variant": "both", - "tags": ["Format: Generic Model", "Mesh", "Tool"] + "creation_date": "2022-04-09", + "tags": ["Format: Generic Model", "Mesh", "Tool"] }, "wasd_controls": { "title": "WASD Controls", diff --git a/plugins/mesh_tools/about.md b/plugins/mesh_tools/about.md index 916a59c5..0f4e1002 100644 --- a/plugins/mesh_tools/about.md +++ b/plugins/mesh_tools/about.md @@ -8,13 +8,13 @@ By installing the plugin, you get: style="padding-inline-start: 0px" >
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
@@ -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.

- -
-
-
- -
Input
-
-
+
+
+
+ +
Input
+
+
-
- -
Result
-
-
+
+ +
Result
+
- +
+ Results with Blend Path enabled. - -
-
-
- -
Input
-
-
+
+
+
+ +
Input
+
+
-
- -
Result
-
-
+
+ +
Result
+
- +
+
@@ -198,38 +196,36 @@ Access From:

Casts selected vertices into a smooth, spherical shape with adjustable influence.

- -
-
-
- -
Input
-
-
+
+
+
+ +
Input
+
+
-
- -
Result
-
-
+
+ +
Result
+
+
+
+ +
+
+
+ +
Input
+
- - -
-
-
- -
Input
-
-
-
- -
Result
-
-
+
+ +
Result
+
- +
+
@@ -248,22 +244,21 @@ Access From:

Smoothens selected vertices by averaging the position of neighboring vertices.

- -
-
-
- -
Input
-
-
+
+
+
+ +
Input
+
+
-
- -
Result
-
-
+
+ +
Result
+
- +
+
@@ -282,22 +277,21 @@ Access From:

Generates a fan out of a face.

- -
-
-
- -
Input
-
-
+
+
+
+ +
Input
+
+
-
- -
Result
-
-
+
+ +
Result
+
- +
+
@@ -316,22 +310,21 @@ Access From:

Attempts to merge adjacent triangles into quadrilaterals.

- -
-
-
- -
Input
-
-
+
+
+
+ +
Input
+
+
-
- -
Result
-
-
+
+ +
Result
+
- +
+
@@ -350,22 +343,21 @@ Access From:

Splits selected faces into triangles.

- -
-
-
- -
Input
-
-
+
+
+
+ +
Input
+
+
-
- -
Result
-
-
+
+ +
Result
+
- +
+
@@ -387,22 +379,21 @@ Access From:

Projects the selected faces to the UV map from the camera.

- -
-
-
- -
Mesh
-
-
+
+
+
+ +
Mesh
+
+
-
- -
UV
-
-
+
+ +
UV
+
- +
+
@@ -424,22 +415,21 @@ Access From:

Unwraps the UV map from the 6 sides of a cube.

- -
-
-
- -
Mesh
-
-
+
+
+
+ +
Mesh
+
+
-
- -
UV
-
-
+
+ +
UV
+
- +
+
@@ -499,22 +489,21 @@ Access From:

Splits the faces of a mesh into smaller faces, giving it a smooth appearance.

- -
-
-
- -
Input
-
-
+
+
+
+ +
Input
+
+
-
- -
Result
-
-
+
+ +
Result
+
- +
+
@@ -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.

- -
-
-
- -
"Butcher" expressed in Chinese
-
-
+
+
+
+ +
"Butcher" expressed in Chinese
+
- +
+
@@ -701,16 +685,15 @@ Access From:

Generates an xyz surface based on mathematical equations containing 23 pre-built presets!

- -
-
-
- -
Twisted Torus Preset
-
-
+
+
+
+ +
Twisted Torus Preset
+
- +
+
@@ -732,16 +715,15 @@ Access From:

Generate a polyhedron such as an Icosahedron, a Dodecahedron, an Octahedron or a Tetrahedron.

- -
-
-
- -
Icosahedron
-
-
+
+
+
+ +
Icosahedron
+
- +
+
@@ -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 ` +
+ +
${translate(caption)}
+
+ `; + } + 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 ` -
- -
${translate(caption)}
-
-`; -} -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 ` +
+ +
${translate(caption)}
+
+ `; +} +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 */