From 50503e36d7985b597661f37e9f293cc3cf81c78d Mon Sep 17 00:00:00 2001 From: JannisX11 Date: Wed, 17 May 2023 18:03:35 +0200 Subject: [PATCH 1/2] Wip mirror modeling --- index.html | 1 + js/modeling/mirror_modeling.js | 187 +++++++++++++++++++++++++++++++++ js/modeling/transform.js | 4 - js/outliner/cube.js | 4 + js/outliner/mesh.js | 10 +- js/undo.js | 1 + 6 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 js/modeling/mirror_modeling.js diff --git a/index.html b/index.html index 6ed91df0..36522711 100644 --- a/index.html +++ b/index.html @@ -139,6 +139,7 @@ + diff --git a/js/modeling/mirror_modeling.js b/js/modeling/mirror_modeling.js new file mode 100644 index 00000000..89c3c332 --- /dev/null +++ b/js/modeling/mirror_modeling.js @@ -0,0 +1,187 @@ +(function() { + + function isCentered(element) { + if (element.origin[0] != 0) return false; + if (element.rotation[1] || element.rotation[2]) return false; + if (element instanceof Cube && !Math.epsilon(element.to[0], -element.from[0], 0.01)) return false; + + let checkParent = (parent) => { + if (parent instanceof Group) { + if (parent.origin[0] != 0) return true; + if (parent.rotation[1] || parent.rotation[2]) return true; + return checkParent(parent.parent); + } + } + if (checkParent(element.parent)) return false; + return true; + } + function createClone(original) { + // Create or update clone + var center = Format.centered_grid ? 0 : 8; + let mirror_element = cached_mirror_elements[original.uuid]?.counterpart; + + if (mirror_element) { + mirror_element.extend(original); + + } else { + let add_to = 'root'; + // todo: figure out add to + mirror_element = new original.constructor(original).addTo(add_to).init(); + } + mirror_element.flip(0, center); + + 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); + } + function createLocalSymmetry(mesh) { + // Create or update clone + let edit_side = Math.sign(Transformer.position.x) || 1; + let deleted_vertices = []; + let deleted_vertex_positions = {}; + for (let vkey in mesh.vertices) { + if (mesh.vertices[vkey][0] && mesh.vertices[vkey][0] * edit_side < 0) { + deleted_vertex_positions[vkey] = mesh.vertices[vkey]; + delete mesh.vertices[vkey]; + deleted_vertices.push(vkey); + } + } + let added_vertices = []; + let vertex_counterpart = {}; + for (let vkey in mesh.vertices) { + let vertex = mesh.vertices[vkey]; + if (mesh.vertices[vkey][0] == 0) { + // On Edge + vertex_counterpart[vkey] = vkey; + } else { + let vkey2 = mesh.addVertices([-vertex[0], vertex[1], vertex[2]])[0]; + added_vertices.push(vkey2); + vertex_counterpart[vkey] = vkey2; + } + } + + for (let fkey in mesh.faces) { + let face = mesh.faces[fkey]; + let deleted_face_vertices = face.vertices.filter(vkey => deleted_vertices.includes(vkey)); + if (deleted_face_vertices.length == face.vertices.length) { + delete mesh.faces[fkey]; + + } 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.includes(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_vertex_positions[vkey][1], 2) + Math.pow(mesh.vertices[a][2] - deleted_vertex_positions[vkey][2], 2); + let b_distance = Math.pow(mesh.vertices[b][1] - deleted_vertex_positions[vkey][1], 2) + Math.pow(mesh.vertices[b][2] - deleted_vertex_positions[vkey][2], 2); + return b_distance - a_distance; + }) + + + let counterpart = new_counterparts.pop(); + face.vertices.splice(i, 1, counterpart); + face.uv[counterpart] = face.uv[vkey]; + delete face.uv[vkey]; + } + }) + + } else if (deleted_face_vertices.length == 0) { + // Recreate face as mirrored + let new_face = new MeshFace(mesh, face); + face.vertices.forEach((vkey, i) => { + new_face.vertices.splice(i, 1, vertex_counterpart[vkey]); + delete new_face.uv[vkey]; + new_face.uv[vertex_counterpart[vkey]] = face.uv[vkey]; + }) + new_face.invert(); + let [face_key] = mesh.addFaces(new_face); + } + + } + let {preview_controller} = mesh; + preview_controller.updateGeometry(mesh); + preview_controller.updateFaces(mesh); + preview_controller.updateUV(mesh); + } + + let cached_mirror_elements = {}; + Blockbench.on('init_edit', ({aspects}) => { + if (!BarItems.mirror_modeling.value) return; + if (!aspects.elements) return; + + cached_mirror_elements = {}; + + aspects.elements.forEach((element) => { + if ((element instanceof Cube || element instanceof Mesh) && element.allow_mirror_modeling) { + let is_centered = isCentered(element); + + cached_mirror_elements[element.uuid] = {is_centered}; + if (!is_centered) { + cached_mirror_elements[element.uuid].counterpart = Painter.getMirrorElement(element, [1, 0, 0]); + } + } + }) + + setTimeout(() => {cached_mirror_elements = {}}, 10_000); + }) + Blockbench.on('finish_edit', ({aspects}) => { + if (!BarItems.mirror_modeling.value) return; + if (!aspects.elements) return; + let last_save = Undo.current_save; + + aspects.elements.forEach((element) => { + if ((element instanceof Cube || element instanceof Mesh) && element.allow_mirror_modeling) { + let is_centered = isCentered(element); + + if (is_centered && element instanceof Mesh) { + // Complete other side of mesh + createLocalSymmetry(element); + } + if (!is_centered) { + // Construct clone at other side of model + createClone(element); + } + } + }) + }) + + // 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() { + 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) { + Cube.selected.concat(Mesh.selected).forEach(element => { + element.allow_mirror_modeling = value; + }) + } + }) + Blockbench.on('update_selection', () => { + if (!Condition(allow_toggle.condition)) return; + let disabled = Cube.selected.find(el => el.allow_mirror_modeling == false) || Mesh.selected.find(el => el.allow_mirror_modeling == false); + if (allow_toggle.value != !disabled) { + allow_toggle.value = !disabled; + allow_toggle.updateEnabledState(); + } + }) + }) + +})() \ No newline at end of file diff --git a/js/modeling/transform.js b/js/modeling/transform.js index 351afacb..d9d8722a 100644 --- a/js/modeling/transform.js +++ b/js/modeling/transform.js @@ -212,10 +212,6 @@ function mirrorSelected(axis) { } selected.forEach(function(obj) { obj.flip(axis, center, false) - if (obj instanceof Cube && obj.box_uv && axis === 0) { - obj.mirror_uv = !obj.mirror_uv - Canvas.updateUV(obj) - } }) updateSelection() Undo.finishEdit('Flip selection') diff --git a/js/outliner/cube.js b/js/outliner/cube.js index ac22046f..d1713403 100644 --- a/js/outliner/cube.js +++ b/js/outliner/cube.js @@ -407,6 +407,9 @@ class Cube extends OutlinerElement { if (!skipUV) { + if (this.box_uv && axis === 0) { + this.mirror_uv = !this.mirror_uv; + } function mirrorUVX(face, skip_rot) { var f = scope.faces[face] if (skip_rot) {} @@ -811,6 +814,7 @@ class Cube extends OutlinerElement { 'convert_to_mesh', 'update_autouv', 'cube_uv_mode', + 'allow_element_mirror_modeling', {name: 'menu.cube.color', icon: 'color_lens', children() { return markerColors.map((color, i) => {return { icon: 'bubble_chart', diff --git a/js/outliner/mesh.js b/js/outliner/mesh.js index 7ab810d5..06492294 100644 --- a/js/outliner/mesh.js +++ b/js/outliner/mesh.js @@ -713,6 +713,7 @@ class Mesh extends OutlinerElement { ...Outliner.control_menu_group, '_', 'rename', + 'allow_element_mirror_modeling', {name: 'menu.cube.color', icon: 'color_lens', children() { return markerColors.map((color, i) => {return { icon: 'bubble_chart', @@ -848,9 +849,12 @@ new NodePreviewController(Mesh, { let index_offset = position_array.length / 3; let face_indices = {}; - face.vertices.forEach((key, i) => { - position_array.push(...element.vertices[key]) - face_indices[key] = index_offset + i; + face.vertices.forEach((vkey, i) => { + if (!element.vertices[vkey]) { + throw new Error(`Face "${key}" in mesh "${element.name}" contains an invalid vertex key "${vkey}"`, face) + } + position_array.push(...element.vertices[vkey]) + face_indices[vkey] = index_offset + i; }) let normal = face.getNormal(true); diff --git a/js/undo.js b/js/undo.js index bc2906bb..9449b9dc 100644 --- a/js/undo.js +++ b/js/undo.js @@ -21,6 +21,7 @@ class UndoSystem { } this.startChange(amended); this.current_save = new UndoSystem.save(aspects) + Blockbench.dispatchEvent('init_edit', {aspects, amended, save: this.current_save}) return this.current_save; } finishEdit(action, aspects) { From d33a7de1f4fc61e68f316a3ec146978ea69ee0da Mon Sep 17 00:00:00 2001 From: JannisX11 Date: Sun, 25 Jun 2023 01:07:52 +0200 Subject: [PATCH 2/2] Mirror Modeling fixes - Add outliner hierarchy support - Undo support --- js/modeling/mirror_modeling.js | 200 ++++++++++++++++++++------------- js/modeling/transform.js | 79 +++++++------ js/outliner/mesh.js | 20 +++- lang/en.json | 4 + 4 files changed, 190 insertions(+), 113 deletions(-) diff --git a/js/modeling/mirror_modeling.js b/js/modeling/mirror_modeling.js index 89c3c332..cb866563 100644 --- a/js/modeling/mirror_modeling.js +++ b/js/modeling/mirror_modeling.js @@ -1,6 +1,5 @@ -(function() { - - function isCentered(element) { +const MirrorModeling = { + isCentered(element) { if (element.origin[0] != 0) return false; if (element.rotation[1] || element.rotation[2]) return false; if (element instanceof Cube && !Math.epsilon(element.to[0], -element.from[0], 0.01)) return false; @@ -14,29 +13,64 @@ } if (checkParent(element.parent)) return false; return true; - } - function createClone(original) { + }, + createClone(original, undo_aspects) { // Create or update clone var center = Format.centered_grid ? 0 : 8; - let mirror_element = cached_mirror_elements[original.uuid]?.counterpart; + let mirror_element = MirrorModeling.cached_elements[original.uuid]?.counterpart; + let element_before_snapshot; if (mirror_element) { + element_before_snapshot = mirror_element.getUndoCopy(undo_aspects); mirror_element.extend(original); } else { - let add_to = 'root'; - // todo: figure out add to + 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] *= -1; + 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); - } - function createLocalSymmetry(mesh) { + }, + createLocalSymmetry(mesh) { // Create or update clone let edit_side = Math.sign(Transformer.position.x) || 1; let deleted_vertices = []; @@ -107,81 +141,95 @@ preview_controller.updateGeometry(mesh); preview_controller.updateFaces(mesh); preview_controller.updateUV(mesh); - } + }, + 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; + } - let cached_mirror_elements = {}; - Blockbench.on('init_edit', ({aspects}) => { - if (!BarItems.mirror_modeling.value) return; - if (!aspects.elements) return; + // post + if (!element_before_snapshot) undo_aspects.outliner = true; + undo_aspects.elements.safePush(element); + }, + cached_elements: {} +} - cached_mirror_elements = {}; +Blockbench.on('init_edit', ({aspects}) => { + if (!BarItems.mirror_modeling.value) return; + if (!aspects.elements) return; - aspects.elements.forEach((element) => { - if ((element instanceof Cube || element instanceof Mesh) && element.allow_mirror_modeling) { - let is_centered = isCentered(element); + MirrorModeling.cached_elements = {}; + MirrorModeling.outliner_snapshot = aspects.outliner ? null : compileGroups(true); - cached_mirror_elements[element.uuid] = {is_centered}; - if (!is_centered) { - cached_mirror_elements[element.uuid].counterpart = Painter.getMirrorElement(element, [1, 0, 0]); - } + aspects.elements.forEach((element) => { + if (element.allow_mirror_modeling) { + let is_centered = MirrorModeling.isCentered(element); + + MirrorModeling.cached_elements[element.uuid] = {is_centered}; + if (!is_centered) { + MirrorModeling.cached_elements[element.uuid].counterpart = Painter.getMirrorElement(element, [1, 0, 0]); } - }) - - setTimeout(() => {cached_mirror_elements = {}}, 10_000); - }) - Blockbench.on('finish_edit', ({aspects}) => { - if (!BarItems.mirror_modeling.value) return; - if (!aspects.elements) return; - let last_save = Undo.current_save; - - aspects.elements.forEach((element) => { - if ((element instanceof Cube || element instanceof Mesh) && element.allow_mirror_modeling) { - let is_centered = isCentered(element); - - if (is_centered && element instanceof Mesh) { - // Complete other side of mesh - createLocalSymmetry(element); - } - if (!is_centered) { - // Construct clone at other side of model - createClone(element); - } - } - }) + } }) - // Element property on cube and mesh - new Property(Cube, 'boolean', 'allow_mirror_modeling', {default: true}); - new Property(Mesh, 'boolean', 'allow_mirror_modeling', {default: true}); + setTimeout(() => {MirrorModeling.cached_elements = {}}, 10_000); +}) +Blockbench.on('finish_edit', ({aspects}) => { + if (!BarItems.mirror_modeling.value) return; + if (!aspects.elements) return; - BARS.defineActions(() => { - - new Toggle('mirror_modeling', { - icon: 'align_horizontal_center', - category: 'edit', - condition: {modes: ['edit']}, - onChange() { - updateSelection(); + aspects.elements = aspects.elements.slice(); + let static_elements_copy = aspects.elements.slice(); + static_elements_copy.forEach((element) => { + if (element.allow_mirror_modeling) { + let is_centered = MirrorModeling.isCentered(element); + + if (is_centered && element instanceof Mesh) { + // Complete other side of mesh + MirrorModeling.createLocalSymmetry(element); } - }) - 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) { - Cube.selected.concat(Mesh.selected).forEach(element => { - element.allow_mirror_modeling = value; - }) + if (!is_centered) { + // Construct clone at other side of model + MirrorModeling.createClone(element, aspects); } - }) - Blockbench.on('update_selection', () => { - if (!Condition(allow_toggle.condition)) return; - let disabled = Cube.selected.find(el => el.allow_mirror_modeling == false) || Mesh.selected.find(el => el.allow_mirror_modeling == false); - if (allow_toggle.value != !disabled) { - allow_toggle.value = !disabled; - allow_toggle.updateEnabledState(); - } - }) + } }) +}) -})() \ No newline at end of file +// 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() { + 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(); + } + }) +}) diff --git a/js/modeling/transform.js b/js/modeling/transform.js index d9d8722a..884dac0b 100644 --- a/js/modeling/transform.js +++ b/js/modeling/transform.js @@ -145,6 +145,43 @@ function rotateSelected(axis, steps) { Undo.finishEdit('Rotate elements') } //Mirror +function flipNameOnAxis(node, axis, check, original_name) { + const flip_pairs = { + 0: { + right: 'left', + Right: 'Left', + RIGHT: 'LEFT', + }, + 1: { + top: 'bottom', + Top: 'Bottom', + TOP: 'BOTTOM', + }, + 2: { + back: 'front', + rear: 'front', + Back: 'Front', + Rear: 'Front', + BACK: 'FRONT', + REAR: 'FRONT', + } + }; + function matchAndReplace(a, b) { + if (node.name.includes(a)) { + let name = original_name + ? original_name.replace(a, b) + : node.name.replace(a, b).replace(/2/, ''); + if (check(name)) node.name = name; + return true; + } + } + let pairs = flip_pairs[axis]; + for (let a in pairs) { + let b = pairs[a]; + if (matchAndReplace(a, b)) break; + if (matchAndReplace(b, a)) break; + } +} function mirrorSelected(axis) { if (Modes.animate && Timeline.selected.length) { @@ -160,26 +197,6 @@ function mirrorSelected(axis) { Undo.initEdit({elements: selected, outliner: Format.bone_rig || Group.selected, selection: true}) var center = Format.centered_grid ? 0 : 8; if (Format.bone_rig) { - let flip_pairs = { - 0: { - right: 'left', - Right: 'Left', - RIGHT: 'LEFT', - }, - 1: { - top: 'bottom', - Top: 'Bottom', - TOP: 'BOTTOM', - }, - 2: { - back: 'front', - rear: 'front', - Back: 'Front', - Rear: 'Front', - BACK: 'FRONT', - REAR: 'FRONT', - } - } if (Group.selected && Group.selected.matchesSelection()) { function flipGroup(group) { for (var i = 0; i < 3; i++) { @@ -189,21 +206,7 @@ function mirrorSelected(axis) { group.rotation[i] *= -1 } } - function matchAndReplace(a, b) { - if (group.name.includes(a)) { - let name = group._original_name - ? group._original_name.replace(a, b) - : group.name.replace(a, b).replace(/2/, ''); - if (!Group.all.find(g => g.name == name)) group.name = name; - return true; - } - } - let pairs = flip_pairs[axis]; - for (let a in pairs) { - let b = pairs[a]; - if (matchAndReplace(a, b)) break; - if (matchAndReplace(b, a)) break; - } + flipNameOnAxis(group, axis, name => (!Group.all.find(g => g.name == name)), group._original_name); Canvas.updateAllBones([group]); } flipGroup(Group.selected) @@ -211,7 +214,11 @@ function mirrorSelected(axis) { } } selected.forEach(function(obj) { - obj.flip(axis, center, false) + if (obj instanceof Mesh) { + obj.flipSelection(axis, center, false); + } else { + obj.flip(axis, center, false); + } }) updateSelection() Undo.finishEdit('Flip selection') diff --git a/js/outliner/mesh.js b/js/outliner/mesh.js index 06492294..ccf17dd7 100644 --- a/js/outliner/mesh.js +++ b/js/outliner/mesh.js @@ -602,7 +602,25 @@ class Mesh extends OutlinerElement { return this; } flip(axis, center) { - let object_mode = BarItems.selection_mode.value == 'object'; + for (let vkey in this.vertices) { + this.vertices[vkey][axis] *= -1; + } + for (let key in this.faces) { + this.faces[key].invert(); + } + + this.origin[axis] *= -1; + this.rotation.forEach((n, i) => { + if (i != axis) this.rotation[i] = -n; + }) + this.preview_controller.updateTransform(this); + + this.preview_controller.updateGeometry(this); + this.preview_controller.updateUV(this); + return this; + } + flipSelection(axis, center) { + let object_mode = BarItems.selection_mode.value == 'object' || !!Group.selected; let selected_vertices = this.getSelectedVertices(); for (let vkey in this.vertices) { if (object_mode || selected_vertices.includes(vkey)) { diff --git a/lang/en.json b/lang/en.json index 68fe7fa9..ffb2f18e 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1319,6 +1319,10 @@ "action.update_autouv.desc": "Update the auto UV mapping of the selected cubes", "action.edit_material_instances": "Edit Material Instances", "action.edit_material_instances.desc": "Edit material instance names for bedrock block geometries", + "action.mirror_modeling": "Mirror Modeling", + "action.mirror_modeling.desc": "Enable Mirror Modeling on the X axis. All changes you make in the viewport will be reflected to the other side, unless specifically disabled on the element.", + "action.allow_element_mirror_modeling": "Allow Mirror Modeling", + "action.allow_element_mirror_modeling.desc": "Choose whether the selected elements can be affected by mirror modeling", "action.selection_mode": "Selection Mode", "action.selection_mode.desc": "Change how elements can be selected in the viewport", "action.selection_mode.object": "Object",