Basic proportional editing

This commit is contained in:
JannisX11 2023-02-21 22:56:44 +01:00
parent 640e59762f
commit 3f6bdf096c
9 changed files with 240 additions and 23 deletions

View File

@ -277,7 +277,11 @@ class Action extends BarItem {
open_node.classList.add('action_more_options');
open_node.onclick = e => {
e.stopPropagation();
this.side_menu.open(e.target.parentElement);
if (this.side_menu instanceof Menu) {
this.side_menu.open(e.target.parentElement);
} else if (this.side_menu instanceof Dialog) {
this.side_menu.show();
}
}
this.node.append(open_node);
}
@ -326,7 +330,11 @@ class Action extends BarItem {
if (options && !options.onclick) {
options.onclick = e => {
e.stopPropagation();
this.side_menu.open(e.target.parentElement);
if (this.side_menu instanceof Menu) {
this.side_menu.open(e.target.parentElement);
} else if (this.side_menu instanceof Dialog) {
this.side_menu.show();
}
}
}
}
@ -2159,6 +2167,7 @@ const BARS = {
'loop_cut',
'create_face',
'invert_face',
'proportional_editing',
]
})

View File

@ -292,9 +292,20 @@ class Menu {
if (!(e.target == entry[0] || e.target.parentElement == entry[0])) return;
s.trigger(e)
});
if (s.side_menu) {
if (s.side_menu instanceof Menu) {
let content_list = typeof s.side_menu.structure == 'function' ? s.side_menu.structure(context) : s.side_menu.structure;
createChildList(s, entry, content_list);
} else if (s.side_menu instanceof Dialog) {
createChildList(s, entry, [
{
name: 'menu.options',
icon: 'web_asset',
click() {
s.side_menu.show();
}
}
]);
}
}
@ -343,6 +354,35 @@ class Menu {
scope.hover(this, e)
})
/*} else if (s instanceof NumSlider) {
let icon = Blockbench.getIconNode(s.icon, s.color);
let numeric_input = new Interface.CustomElements.NumericInput(s.id, {
value: s.get(),
min: s.settings?.min, max: s.settings?.max,
onChange(value) {
if (typeof s.onBefore === 'function') {
s.onBefore()
}
s.change(() => value);
if (typeof s.onAfter === 'function') {
s.onAfter()
}
s.update();
}
});
entry = Interface.createElement('li', {title: s.description && tl(s.description), menu_item: s.id}, [
Interface.createElement('span', {}, tl(s.name)),
numeric_input.node
]);
entry.prepend(icon);
parent.append(entry);
$(entry).mouseenter(function(e) {
scope.hover(this, e)
})
*/
} else if (s instanceof HTMLElement) {
parent.append(s);

View File

@ -254,6 +254,8 @@ const MenuBar = {
'dissolve_edges',
'split_mesh',
'merge_meshes',
'_',
'proportional_editing',
]},
'_',
'select_window',

View File

@ -2,6 +2,86 @@ function sameMeshEdge(edge_a, edge_b) {
return edge_a.equals(edge_b) || (edge_a[0] == edge_b[1] && edge_a[1] == edge_b[0])
}
function proportionallyEditMeshVertices(mesh, callback) {
if (!BarItems.proportional_editing.value) return;
let selected_vertices = mesh.getSelectedVertices();
let {range, shape, selection} = StateMemory.proportional_editing_options;
let linear_distance = selection == 'linear';
let all_mesh_connections;
if (!linear_distance) {
all_mesh_connections = {};
for (let fkey in mesh.faces) {
let face = mesh.faces[fkey];
face.getEdges().forEach(edge => {
if (!all_mesh_connections[edge[0]]) {
all_mesh_connections[edge[0]] = [edge[1]];
} else {
all_mesh_connections[edge[0]].safePush(edge[1]);
}
if (!all_mesh_connections[edge[1]]) {
all_mesh_connections[edge[1]] = [edge[0]];
} else {
all_mesh_connections[edge[1]].safePush(edge[0]);
}
})
}
}
for (let vkey in mesh.vertices) {
if (selected_vertices.includes(vkey)) continue;
let distance = Infinity;
if (linear_distance) {
// Linear Distance
selected_vertices.forEach(vkey2 => {
let pos1 = mesh.vertices[vkey];
let pos2 = mesh.vertices[vkey2];
let distance_square = Math.pow(pos1[0] - pos2[0], 2) + Math.pow(pos1[1] - pos2[1], 2) + Math.pow(pos1[2] - pos2[2], 2);
if (distance_square < distance) {
distance = distance_square;
}
})
distance = Math.sqrt(distance);
} else {
// Connection Distance
let found_match_depth = 0;
let scanned = [];
let frontier = [vkey];
depth_crawler:
for (let depth = 1; depth <= range; depth++) {
let new_frontier = [];
for (let vkey1 of frontier) {
let connections = all_mesh_connections[vkey1]?.filter(vkey2 => !scanned.includes(vkey2));
if (!connections || connections.length == 0) continue;
scanned.push(...connections);
new_frontier.push(...connections);
}
for (let vkey2 of new_frontier) {
if (selected_vertices.includes(vkey2)) {
found_match_depth = depth;
break depth_crawler;
}
}
frontier = new_frontier;
}
if (found_match_depth) {
distance = found_match_depth;
}
}
if (distance > range) continue;
let blend = 1 - (distance / (linear_distance ? range : range+1));
switch (shape) {
case 'hermite_spline': blend = Math.hermiteBlend(blend); break;
case 'constant': blend = 1; break;
}
callback(vkey, blend);
}
}
BARS.defineActions(function() {
let add_mesh_dialog = new Dialog({
id: 'add_primitive',
@ -1711,4 +1791,65 @@ BARS.defineActions(function() {
})
}
})
StateMemory.init('proportional_editing_options', 'object');
if (!StateMemory.proportional_editing_options.range) {
StateMemory.proportional_editing_options.range = 8;
}
if (!StateMemory.proportional_editing_options.shape) {
StateMemory.proportional_editing_options.shape = 'linear';
}
if (!StateMemory.proportional_editing_options.selection) {
StateMemory.proportional_editing_options.selection = 'linear';
}
new NumSlider('proportional_editing_range', {
category: 'edit',
condition: {modes: ['edit'], features: ['meshes']},
get() {
return StateMemory.proportional_editing_options.range
},
change(modify) {
StateMemory.proportional_editing_options.range = modify(StateMemory.proportional_editing_options.range);
},
onAfter() {
StateMemory.save('proportional_editing_options');
}
})
new Toggle('proportional_editing', {
icon: 'wifi_tethering',
category: 'edit',
condition: {modes: ['edit'], features: ['meshes'], method: () => (Mesh.selected[0] && Mesh.selected[0].getSelectedVertices().length > 0)},
side_menu: new Dialog('proportional_editing_options', {
title: 'action.proportional_editing',
width: 400,
singleButton: true,
form: {
enabled: {type: 'checkbox', label: 'menu.mirror_painting.enabled', value: false},
range: {type: 'number', label: 'Range', value: StateMemory.proportional_editing_options.range},
shape: {type: 'select', label: 'Falloff', value: StateMemory.proportional_editing_options.shape, options: {
linear: 'Linear',
hermite_spline: 'Smooth',
constant: 'Constant',
}},
selection: {type: 'select', label: 'Selection', value: StateMemory.proportional_editing_options.selection, options: {
linear: 'Linear Distance',
connections: 'Connections',
//path: 'Connection Path',
}},
},
onOpen() {
this.setFormValues({enabled: BarItems.proportional_editing.value});
},
onFormChange(formResult) {
if (BarItems.proportional_editing.value != formResult.enabled) {
BarItems.proportional_editing.trigger();
}
StateMemory.proportional_editing_options.range = formResult.range;
StateMemory.proportional_editing_options.shape = formResult.shape;
StateMemory.proportional_editing_options.selection = formResult.selection;
StateMemory.save('proportional_editing_options');
}
})
})
})

View File

@ -560,25 +560,31 @@ function moveElementsInSpace(difference, axis) {
let selection_rotation = space == 3 && el.getSelectionRotation();
let selected_vertices = el.getSelectedVertices();
if (!selected_vertices.length) selected_vertices = Object.keys(el.vertices)
if (!selected_vertices.length) selected_vertices = Object.keys(el.vertices);
let difference_vec = [0, 0, 0];
if (space == 2) {
difference_vec[axis] += difference;
} else if (space == 3) {
let m = vector.set(0, 0, 0);
m[getAxisLetter(axis)] = difference;
m.applyEuler(selection_rotation);
difference_vec.V3_set(m.x, m.y, m.z);
} else {
let m = vector.set(0, 0, 0);
m[getAxisLetter(axis)] = difference;
m.applyQuaternion(el.mesh.getWorldQuaternion(quaternion).invert());
difference_vec.V3_set(m.x, m.y, m.z);
}
selected_vertices.forEach(key => {
if (space == 2) {
el.vertices[key][axis] += difference;
} else if (space == 3) {
let m = vector.set(0, 0, 0);
m[getAxisLetter(axis)] = difference;
m.applyEuler(selection_rotation);
el.vertices[key].V3_add(m.x, m.y, m.z);
} else {
let m = vector.set(0, 0, 0);
m[getAxisLetter(axis)] = difference;
m.applyQuaternion(el.mesh.getWorldQuaternion(quaternion).invert());
el.vertices[key].V3_add(m.x, m.y, m.z);
}
el.vertices[key].V3_add(difference_vec);
})
proportionallyEditMeshVertices(el, (vkey, blend) => {
console.log(vkey, blend)
el.vertices[vkey].V3_add(difference_vec[0] * blend, difference_vec[1] * blend, difference_vec[2] * blend);
})
} else {

View File

@ -191,6 +191,19 @@ class MeshFace extends Face {
}
return vertices;
}
getEdges() {
let vertices = this.getSortedVertices();
if (vertices.length == 2) {
return vertices;
} else if (vertices.length > 2) {
return vertices.map((vkey1, i) => {
let vkey2 = vertices[i+1] || vertices[0];
return [vkey1, vkey2];
})
} else {
return [];
}
}
getAdjacentFace(side_index = 0) {
let vertices = this.getSortedVertices();
side_index = side_index % this.vertices.length;

View File

@ -1397,7 +1397,7 @@ const Painter = {
var distance = Math.sqrt(v_px*v_px + v_py*v_py)
if (soft*r != 0) {
var pos_on_gradient = Math.clamp((distance-(1-soft)*r) / (soft*r), 0, 1)
pos_on_gradient = 3*Math.pow(pos_on_gradient, 2) - 2*Math.pow(pos_on_gradient, 3);
pos_on_gradient = Math.hermiteBlend(pos_on_gradient);
} else {
if (r < 8) {
distance *= 1.2;
@ -2206,7 +2206,7 @@ BARS.defineActions(function() {
side_menu: new Menu('mirror_painting', [
// Enabled
{
name: 'Enabled',
name: 'menu.mirror_painting.enabled',
icon: () => Painter.mirror_painting,
click() {BarItems.mirror_painting.trigger()}
},

View File

@ -256,6 +256,9 @@ Math.snapToValues = function(val, snap_points, epsilon = 12) {
return val
}
}
Math.hermiteBlend = function(input) {
return 3*Math.pow(input, 2) - 2*Math.pow(input, 3);
}
function trimFloatNumber(val, max_digits = 4) {
if (val == '') return val;
var string = val.toFixed(max_digits)

View File

@ -1603,6 +1603,8 @@
"menu.action_control.recent_in_streamer_mode": "Sure? Streamer Mode is enabled",
"menu.action_control.recent_in_streamer_mode.desc": "Are you sure you want to display recent models?",
"menu.options": "Options...",
"menu.file": "File",
"menu.edit": "Edit",
"menu.transform": "Transform",
@ -1731,6 +1733,7 @@
"menu.uv.flip_y": "Mirror Y",
"menu.uv.texture": "Texture",
"menu.mirror_painting.enabled": "Enabled",
"menu.mirror_painting.axis": "Axis",
"menu.mirror_painting.axis.desc": "Select on which axis paint strokes will get mirrored",
"menu.mirror_painting.global": "Global Symmetry",