mirror of
https://github.com/JannisX11/blockbench.git
synced 2024-11-21 01:13:37 +08:00
449 lines
16 KiB
JavaScript
449 lines
16 KiB
JavaScript
const MirrorModeling = {
|
|
initial_transformer_position: 0,
|
|
isCentered(element) {
|
|
let center = Format.centered_grid ? 0 : 8;
|
|
if (!element.to && Math.roundTo(element.origin[0], 3) != center) return false;
|
|
if (Math.roundTo(element.rotation[1], 3) || Math.roundTo(element.rotation[2], 3)) return false;
|
|
if (element instanceof Cube && !Math.epsilon(element.to[0], MirrorModeling.flipCoord(element.from[0]), 0.01)) return false;
|
|
|
|
let checkParent = (parent) => {
|
|
if (parent instanceof Group) {
|
|
if (parent.origin[0] != center) return true;
|
|
if (parent.rotation[1] || parent.rotation[2]) return true;
|
|
return checkParent(parent.parent);
|
|
}
|
|
}
|
|
if (checkParent(element.parent)) return false;
|
|
return true;
|
|
},
|
|
createClone(original, undo_aspects) {
|
|
// Create or update clone
|
|
let center = Format.centered_grid ? 0 : 8;
|
|
let mirror_element = MirrorModeling.cached_elements[original.uuid]?.counterpart;
|
|
let element_before_snapshot;
|
|
|
|
if (mirror_element == original) return;
|
|
|
|
if (mirror_element) {
|
|
element_before_snapshot = mirror_element.getUndoCopy(undo_aspects);
|
|
mirror_element.extend(original);
|
|
|
|
mirror_element.flip(0, center);
|
|
|
|
mirror_element.extend({
|
|
name: element_before_snapshot.name
|
|
});
|
|
|
|
// Update hierarchy up
|
|
function updateParent(child, child_b) {
|
|
let parent = child.parent;
|
|
let parent_b = child_b.parent;
|
|
if (parent instanceof Group == false || parent == parent_b) return;
|
|
|
|
MirrorModeling.updateGroupCounterpart(parent_b, parent);
|
|
|
|
updateParent(parent, parent_b);
|
|
}
|
|
updateParent(original, mirror_element);
|
|
|
|
} else {
|
|
function getParentMirror(child) {
|
|
let parent = child.parent;
|
|
if (parent instanceof Group == false) return 'root';
|
|
|
|
if (parent.origin[0] == center) {
|
|
return parent;
|
|
} else {
|
|
let mirror_group_parent = getParentMirror(parent);
|
|
let mirror_group = new Group(parent);
|
|
|
|
flipNameOnAxis(mirror_group, 0, name => true, parent.name);
|
|
mirror_group.origin[0] = MirrorModeling.flipCoord(mirror_group.origin[0]);
|
|
mirror_group.rotation[1] *= -1;
|
|
mirror_group.rotation[2] *= -1;
|
|
mirror_group.isOpen = parent.isOpen;
|
|
|
|
let parent_list = mirror_group_parent instanceof Group ? mirror_group_parent.children : Outliner.root;
|
|
let match = parent_list.find(node => {
|
|
if (node instanceof Group == false) return false;
|
|
if (node.name == mirror_group.name && node.rotation.equals(mirror_group.rotation) && node.origin.equals(mirror_group.origin)) {
|
|
return true;
|
|
}
|
|
})
|
|
if (match) {
|
|
return match;
|
|
} else {
|
|
mirror_group.createUniqueName();
|
|
mirror_group.addTo(mirror_group_parent).init();
|
|
return mirror_group;
|
|
}
|
|
}
|
|
}
|
|
let add_to = getParentMirror(original);
|
|
mirror_element = new original.constructor(original).addTo(add_to).init();
|
|
mirror_element.flip(0, center);
|
|
}
|
|
|
|
MirrorModeling.insertElementIntoUndo(mirror_element, undo_aspects, element_before_snapshot);
|
|
|
|
let {preview_controller} = mirror_element;
|
|
preview_controller.updateTransform(mirror_element);
|
|
preview_controller.updateGeometry(mirror_element);
|
|
preview_controller.updateFaces(mirror_element);
|
|
preview_controller.updateUV(mirror_element);
|
|
preview_controller.updateVisibility(mirror_element);
|
|
return mirror_element;
|
|
},
|
|
updateGroupCounterpart(group, original) {
|
|
let keep_properties = {
|
|
name: group.name
|
|
};
|
|
group.extend(original);
|
|
group.extend(keep_properties);
|
|
|
|
//flipNameOnAxis(group, 0, name => true, original.name);
|
|
group.origin[0] = MirrorModeling.flipCoord(group.origin[0]);
|
|
group.rotation[1] *= -1;
|
|
group.rotation[2] *= -1;
|
|
},
|
|
getEditSide() {
|
|
return Math.sign(Transformer.position.x || MirrorModeling.initial_transformer_position) || 1;
|
|
},
|
|
flipCoord(input) {
|
|
if (Format.centered_grid) {
|
|
return -input;
|
|
} else {
|
|
return 16 - input;
|
|
}
|
|
},
|
|
createLocalSymmetry(mesh) {
|
|
// Create or update clone
|
|
let edit_side = MirrorModeling.getEditSide();
|
|
// Delete all vertices on the non-edit side
|
|
let deleted_vertices = {};
|
|
let deleted_vertices_by_position = {};
|
|
function positionKey(position) {
|
|
return position.map(p => Math.round(p*25)/25).join(',');
|
|
}
|
|
for (let vkey in mesh.vertices) {
|
|
if (mesh.vertices[vkey][0] && Math.round(mesh.vertices[vkey][0] * edit_side * 50) < 0) {
|
|
deleted_vertices[vkey] = mesh.vertices[vkey];
|
|
delete mesh.vertices[vkey];
|
|
deleted_vertices_by_position[positionKey(deleted_vertices[vkey])] = vkey;
|
|
}
|
|
}
|
|
// Copy existing vertices back to the non-edit side
|
|
let added_vertices = [];
|
|
let vertex_counterpart = {};
|
|
let center_vertices = [];
|
|
for (let vkey in mesh.vertices) {
|
|
let vertex = mesh.vertices[vkey];
|
|
if (Math.abs(mesh.vertices[vkey][0]) < 0.02) {
|
|
// On Edge
|
|
vertex_counterpart[vkey] = vkey;
|
|
center_vertices.push(vkey);
|
|
} else {
|
|
let position = [MirrorModeling.flipCoord(vertex[0]), vertex[1], vertex[2]];
|
|
let vkey_new = deleted_vertices_by_position[positionKey(position)];
|
|
if (vkey_new) {
|
|
mesh.vertices[vkey_new] = position;
|
|
} else {
|
|
vkey_new = mesh.addVertices(position)[0];
|
|
}
|
|
added_vertices.push(vkey_new);
|
|
vertex_counterpart[vkey] = vkey_new;
|
|
}
|
|
}
|
|
|
|
let deleted_faces = {};
|
|
for (let fkey in mesh.faces) {
|
|
let face = mesh.faces[fkey];
|
|
let deleted_face_vertices = face.vertices.filter(vkey => deleted_vertices[vkey] || center_vertices.includes(vkey));
|
|
if (deleted_face_vertices.length == face.vertices.length && !face.vertices.allAre(vkey => center_vertices.includes(vkey))) {
|
|
deleted_faces[fkey] = mesh.faces[fkey];
|
|
delete mesh.faces[fkey];
|
|
}
|
|
}
|
|
|
|
let original_fkeys = Object.keys(mesh.faces);
|
|
for (let fkey of original_fkeys) {
|
|
let face = mesh.faces[fkey];
|
|
let deleted_face_vertices = face.vertices.filter(vkey => deleted_vertices[vkey]);
|
|
if (deleted_face_vertices.length && face.vertices.length != deleted_face_vertices.length*2 && face.vertices.filter(vkey => center_vertices.includes(vkey)).length + deleted_face_vertices.length*2 != face.vertices.length) {
|
|
// cannot flip. restore vertices instead?
|
|
deleted_face_vertices.forEach(vkey => {
|
|
mesh.vertices[vkey] = deleted_vertices[vkey];
|
|
//delete deleted_vertices[vkey];
|
|
})
|
|
|
|
} else if (deleted_face_vertices.length) {
|
|
// face across zero line
|
|
//let kept_face_keys = face.vertices.filter(vkey => mesh.vertices[vkey] != 0 && !deleted_face_vertices.includes(vkey));
|
|
let new_counterparts = face.vertices.filter(vkey => !deleted_vertices[vkey]).map(vkey => vertex_counterpart[vkey]);
|
|
face.vertices.forEach((vkey, i) => {
|
|
if (deleted_face_vertices.includes(vkey)) {
|
|
// Across
|
|
//let kept_key = kept_face_keys[i%kept_face_keys.length];
|
|
new_counterparts.sort((a, b) => {
|
|
let a_distance = Math.pow(mesh.vertices[a][1] - deleted_vertices[vkey][1], 2) + Math.pow(mesh.vertices[a][2] - deleted_vertices[vkey][2], 2);
|
|
let b_distance = Math.pow(mesh.vertices[b][1] - deleted_vertices[vkey][1], 2) + Math.pow(mesh.vertices[b][2] - deleted_vertices[vkey][2], 2);
|
|
return b_distance - a_distance;
|
|
})
|
|
|
|
let counterpart = new_counterparts.pop();
|
|
if (vkey != counterpart && counterpart) {
|
|
face.vertices.splice(i, 1, counterpart);
|
|
face.uv[counterpart] = face.uv[vkey].slice();
|
|
delete face.uv[vkey];
|
|
}
|
|
}
|
|
})
|
|
|
|
} else if (deleted_face_vertices.length == 0 && face.vertices.find((vkey) => vkey != vertex_counterpart[vkey])) {
|
|
// Recreate face as mirrored
|
|
let new_face_key;
|
|
for (let key in deleted_faces) {
|
|
let deleted_face = deleted_faces[key];
|
|
if (face.vertices.allAre(vkey => deleted_face.vertices.includes(vertex_counterpart[vkey]))) {
|
|
new_face_key = key;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let new_face = new MeshFace(mesh, face);
|
|
face.vertices.forEach((vkey, i) => {
|
|
let new_vkey = vertex_counterpart[vkey];
|
|
new_face.vertices.splice(i, 1, new_vkey);
|
|
delete new_face.uv[vkey];
|
|
new_face.uv[new_vkey] = face.uv[vkey].slice();
|
|
})
|
|
new_face.invert();
|
|
if (new_face_key) {
|
|
mesh.faces[new_face_key] = new_face;
|
|
} else {
|
|
[new_face_key] = mesh.addFaces(new_face);
|
|
}
|
|
}
|
|
|
|
}
|
|
let {preview_controller} = mesh;
|
|
preview_controller.updateGeometry(mesh);
|
|
preview_controller.updateFaces(mesh);
|
|
preview_controller.updateUV(mesh);
|
|
},
|
|
getMirrorElement(element) {
|
|
let center = Format.centered_grid ? 0 : 8;
|
|
let e = 0.01;
|
|
let symmetry_axes = [0];
|
|
let off_axes = [ 1, 2];
|
|
function getElementParents(el) {
|
|
let list = [];
|
|
let subject = el;
|
|
while (subject.parent instanceof Group) {
|
|
subject = subject.parent;
|
|
list.push(subject)
|
|
}
|
|
return list;
|
|
}
|
|
if (element instanceof Cube) {
|
|
if (
|
|
symmetry_axes.find((axis) => !Math.epsilon(element.from[axis]-center, center-element.to[axis], e)) == undefined &&
|
|
off_axes.find(axis => element.rotation[axis]) == undefined &&
|
|
getElementParents(element).allAre(group => off_axes.find(axis => group.rotation[axis]) == undefined)
|
|
) {
|
|
return element;
|
|
} else {
|
|
for (var element2 of Cube.all) {
|
|
if (
|
|
element2 != element &&
|
|
Math.epsilon(element.inflate, element2.inflate, e) &&
|
|
off_axes.find(axis => !Math.epsilon(element.from[axis], element2.from[axis], e)) == undefined &&
|
|
off_axes.find(axis => !Math.epsilon(element.to[axis], element2.to[axis], e)) == undefined &&
|
|
symmetry_axes.find(axis => !Math.epsilon(element.size(axis), element2.size(axis), e)) == undefined &&
|
|
symmetry_axes.find(axis => !Math.epsilon(element.to[axis]-center, center-element2.from[axis], e)) == undefined &&
|
|
symmetry_axes.find(axis => !Math.epsilon(element.rotation[axis], element2.rotation[axis], e)) == undefined
|
|
) {
|
|
return element2;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
} else if (element instanceof Mesh) {
|
|
let ep = 0.5;
|
|
let this_center = element.getCenter(true);
|
|
if (
|
|
symmetry_axes.find((axis) => !Math.epsilon(element.origin[axis], center, e)) == undefined &&
|
|
symmetry_axes.find((axis) => !Math.epsilon(this_center[axis], center, ep)) == undefined &&
|
|
off_axes.find(axis => element.rotation[axis]) == undefined
|
|
) {
|
|
return element;
|
|
} else {
|
|
for (var element2 of Mesh.all) {
|
|
let other_center = element2.getCenter(true);
|
|
if (Object.keys(element.vertices).length !== Object.keys(element2.vertices).length) continue;
|
|
if (
|
|
element2 != element &&
|
|
symmetry_axes.find(axis => !Math.epsilon(element.origin[axis]-center, center-element2.origin[axis], e)) == undefined &&
|
|
symmetry_axes.find(axis => !Math.epsilon(this_center[axis]-center, center-other_center[axis], ep)) == undefined &&
|
|
off_axes.find(axis => !Math.epsilon(element.origin[axis], element2.origin[axis], e)) == undefined &&
|
|
off_axes.find(axis => !Math.epsilon(this_center[axis], other_center[axis], ep)) == undefined
|
|
) {
|
|
return element2;
|
|
}
|
|
}
|
|
}
|
|
return element;
|
|
}
|
|
},
|
|
insertElementIntoUndo(element, undo_aspects, element_before_snapshot) {
|
|
// pre
|
|
if (element_before_snapshot) {
|
|
if (!Undo.current_save.elements[element.uuid]) Undo.current_save.elements[element.uuid] = element_before_snapshot;
|
|
} else {
|
|
if (!Undo.current_save.outliner) Undo.current_save.outliner = MirrorModeling.outliner_snapshot;
|
|
}
|
|
|
|
// post
|
|
if (!element_before_snapshot) undo_aspects.outliner = true;
|
|
undo_aspects.elements.safePush(element);
|
|
},
|
|
cached_elements: {}
|
|
}
|
|
|
|
Blockbench.on('init_edit', ({aspects}) => {
|
|
if (!BarItems.mirror_modeling.value) return;
|
|
|
|
MirrorModeling.initial_transformer_position = Transformer.position.x;
|
|
|
|
if (aspects.elements) {
|
|
MirrorModeling.cached_elements = {};
|
|
MirrorModeling.outliner_snapshot = aspects.outliner ? null : compileGroups(true);
|
|
let edit_side = MirrorModeling.getEditSide();
|
|
|
|
aspects.elements.forEach((element) => {
|
|
if (element.allow_mirror_modeling) {
|
|
let is_centered = MirrorModeling.isCentered(element);
|
|
|
|
let data = MirrorModeling.cached_elements[element.uuid] = {is_centered};
|
|
if (!is_centered) {
|
|
data.is_copy = Math.sign(element.getWorldCenter().x) != edit_side;
|
|
data.counterpart = MirrorModeling.getMirrorElement(element);
|
|
if (!data.counterpart) data.is_copy = false;
|
|
} else {
|
|
data.is_copy = false;
|
|
}
|
|
}
|
|
})
|
|
} else if (aspects.group || aspects.outliner) {
|
|
MirrorModeling.cached_elements = {};
|
|
let edit_side = MirrorModeling.getEditSide();
|
|
let selected_groups = aspects.outliner ? Group.all.filter(g => g.selected) : [aspects.group];
|
|
|
|
// update undo
|
|
if (!Undo.current_save.outliner) Undo.current_save.outliner = compileGroups(true);
|
|
aspects.outliner = true;
|
|
|
|
selected_groups.forEach(group => {
|
|
if (group.origin[0] == (Format.centered_grid ? 0 : 8)) return;
|
|
|
|
let mirror_group = Group.all.find(g => {
|
|
if (
|
|
Math.epsilon(group.origin[0], MirrorModeling.flipCoord(g.origin[0])) &&
|
|
Math.epsilon(group.origin[1], g.origin[1]) &&
|
|
Math.epsilon(group.origin[2], g.origin[2]) &&
|
|
group.getDepth() == g.getDepth()
|
|
) {
|
|
return true;
|
|
}
|
|
})
|
|
|
|
if (mirror_group) {
|
|
MirrorModeling.cached_elements[group.uuid] = {
|
|
counterpart: mirror_group
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
Blockbench.on('finish_edit', ({aspects}) => {
|
|
if (!BarItems.mirror_modeling.value) return;
|
|
|
|
if (aspects.elements) {
|
|
aspects.elements = aspects.elements.slice();
|
|
let static_elements_copy = aspects.elements.slice();
|
|
static_elements_copy.forEach((element) => {
|
|
if (element.allow_mirror_modeling && !element.locked) {
|
|
let is_centered = MirrorModeling.isCentered(element);
|
|
|
|
if (is_centered && element instanceof Mesh) {
|
|
// Complete other side of mesh
|
|
MirrorModeling.createLocalSymmetry(element);
|
|
}
|
|
if (is_centered) {
|
|
let mirror_element = MirrorModeling.cached_elements[element.uuid]?.counterpart;
|
|
if (mirror_element && mirror_element.uuid != element.uuid) {
|
|
MirrorModeling.insertElementIntoUndo(mirror_element, Undo.current_save.aspects, mirror_element.getUndoCopy());
|
|
mirror_element.remove();
|
|
aspects.elements.remove(mirror_element);
|
|
}
|
|
} else {
|
|
// Construct clone at other side of model
|
|
MirrorModeling.createClone(element, aspects);
|
|
}
|
|
}
|
|
})
|
|
if (aspects.group || aspects.outliner) {
|
|
Canvas.updateAllBones();
|
|
}
|
|
} else if (aspects.group || aspects.outliner) {
|
|
let selected_groups = aspects.outliner ? Group.all.filter(g => g.selected) : [aspects.group];
|
|
|
|
selected_groups.forEach(group => {
|
|
let mirror_group = MirrorModeling.cached_elements[group.uuid]?.counterpart;
|
|
if (mirror_group) {
|
|
MirrorModeling.updateGroupCounterpart(mirror_group, group);
|
|
}
|
|
})
|
|
|
|
aspects.outliner = true;
|
|
Canvas.updateAllBones();
|
|
}
|
|
})
|
|
|
|
// Element property on cube and mesh
|
|
new Property(Cube, 'boolean', 'allow_mirror_modeling', {default: true});
|
|
new Property(Mesh, 'boolean', 'allow_mirror_modeling', {default: true});
|
|
|
|
BARS.defineActions(() => {
|
|
|
|
new Toggle('mirror_modeling', {
|
|
icon: 'align_horizontal_center',
|
|
category: 'edit',
|
|
condition: {modes: ['edit']},
|
|
onChange() {
|
|
Project.mirror_modeling_enabled = this.value;
|
|
MirrorModeling.cached_elements = {};
|
|
updateSelection();
|
|
}
|
|
})
|
|
let allow_toggle = new Toggle('allow_element_mirror_modeling', {
|
|
icon: 'align_horizontal_center',
|
|
category: 'edit',
|
|
condition: {modes: ['edit'], selected: {element: true}, method: () => BarItems.mirror_modeling.value},
|
|
onChange(value) {
|
|
Outliner.selected.forEach(element => {
|
|
if (!element.constructor.properties.allow_mirror_modeling) return;
|
|
element.allow_mirror_modeling = value;
|
|
})
|
|
}
|
|
})
|
|
Blockbench.on('update_selection', () => {
|
|
if (!Condition(allow_toggle.condition)) return;
|
|
let disabled = Outliner.selected.find(el => el.allow_mirror_modeling === false);
|
|
if (allow_toggle.value != !disabled) {
|
|
allow_toggle.value = !disabled;
|
|
allow_toggle.updateEnabledState();
|
|
}
|
|
})
|
|
})
|