Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
400e8ec
feat: Phase 4 — Bevel Edges (Cmd+B) with welded cube primitive
fernandotonon Apr 17, 2026
06a8eed
feat: interactive bevel gizmo + width cap + gizmo polish
fernandotonon Apr 18, 2026
14c33a0
feat(bevel): symmetric offset + per-face scaffold for multi-face corners
fernandotonon Apr 18, 2026
0b88ae5
feat(bevel): multi-face corner fan + proper shared-vertex wiring
fernandotonon Apr 18, 2026
3b8c874
fix(bevel): trim neighbor face corners on welded primitives
fernandotonon Apr 18, 2026
423d40f
fix(bevel): remove degenerate-tri scaffold, add chamfer-end cap tracking
fernandotonon Apr 18, 2026
99a3855
test(bevel): add smooth-character repro fixtures (disabled)
fernandotonon Apr 18, 2026
a4d07f3
fix(bevel): correct cap-fan winding for smooth character meshes
fernandotonon Apr 18, 2026
8276ac4
fix(bevel): collapse cap polygon when ring-walk offsets aren't covered
fernandotonon Apr 18, 2026
1f8d1ba
docs(bevel): add detailed analysis to DISABLED_RandomSmoothFan test
fernandotonon Apr 19, 2026
a70ea6e
fix(bevel): post-pass hole filler for bevel-introduced boundaries
fernandotonon Apr 19, 2026
c2ecb6f
fix(bevel): pick fill winding by checking for directed-edge conflicts
fernandotonon Apr 19, 2026
dcea5ce
fix(bevel): hole-filler walks multi-out-edge junction vertices correctly
fernandotonon Apr 19, 2026
519564b
fix(bevel): widen hole-filler zone + geometry-first winding
fernandotonon Apr 19, 2026
6f5fe25
fix(bevel): close submesh-seam cracks via position-coincident zone
fernandotonon Apr 19, 2026
7c57817
fix(bevel): promote half-in-zone seam vertices to catch Phase-5 gaps
fernandotonon Apr 19, 2026
e71b472
fix(bevel): walker recovers outer loop after figure-8 junction split
fernandotonon Apr 19, 2026
a4d7698
fix(bevel): detect outer-loop close after subloop extract; salvage se…
fernandotonon Apr 20, 2026
fdf3d9d
chore(bevel): tighten hole-filler comments; bump version to 2.28.0
fernandotonon Apr 20, 2026
fb0a440
fix(bevel): CI link + review fixes
fernandotonon Apr 20, 2026
d5c8df9
fix(bevel, gizmos): address CI, SonarCloud, and gizmo review feedback
fernandotonon Apr 20, 2026
8cbc0b6
fix(primitives): register edited cube mesh under the primitive's cano…
fernandotonon Apr 20, 2026
175a7db
fix(ci): delete DISABLED_ bevel tests so HalfEdgeMeshStandalone has z…
fernandotonon Apr 20, 2026
c5084e1
fix(transform, tests): address CodeRabbit review feedback
fernandotonon Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ cmake_minimum_required(VERSION 3.24.0)
cmake_policy(SET CMP0005 NEW)
cmake_policy(SET CMP0048 NEW) # manages project version

project(QtMeshEditor VERSION 2.27.1 LANGUAGES C CXX)
project(QtMeshEditor VERSION 2.28.0 LANGUAGES C CXX)
message(STATUS "Building QtMeshEditor version ${PROJECT_VERSION}")

set(QTMESHEDITOR_VERSION_STRING "\"${PROJECT_VERSION}\"")
Expand Down
15 changes: 15 additions & 0 deletions qml/PropertiesPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,21 @@ Rectangle {
}
}

// Bevel button (edge mode only)
Rectangle {
property string shortcutLabel: Qt.platform.os === "osx" ? "Cmd+B" : "Ctrl+B"
width: parent.width - 16; height: 26; radius: 3
visible: EditModeController.editModeActive && EditModeController.selectionMode === 1
color: bevelMouse.pressed ? Qt.darker(PropertiesPanelController.highlightColor, 1.2)
: bevelMouse.containsMouse ? Qt.lighter(PropertiesPanelController.highlightColor, 1.1)
: PropertiesPanelController.highlightColor
Text { anchors.centerIn: parent; text: "Bevel (" + parent.shortcutLabel + ")"; color: "white"; font.pixelSize: 11 }
MouseArea {
id: bevelMouse; anchors.fill: parent; hoverEnabled: true
onClicked: EditModeController.bevelSelection()
}
}

// Separator
Rectangle { width: parent.width - 16; height: 1; color: PropertiesPanelController.borderColor }

Expand Down
224 changes: 224 additions & 0 deletions src/BevelGizmo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
-----------------------------------------------------------------------------------
A QtMeshEditor file

Copyright (c) Fernando Tonon (https://github.com/fernandotonon)
MIT License — see BevelGizmo.h
-----------------------------------------------------------------------------------
*/

#include "BevelGizmo.h"
#include "GlobalDefinitions.h"

#include <OgreSceneManager.h>
#include <OgreSceneNode.h>
#include <OgreManualObject.h>
#include <OgreRay.h>
#include <OgreQuaternion.h>
#include <OgreCamera.h>
#include <cmath>

namespace
{
// Axis-aligned cube centered at origin with half-side s, emitted into a
// triangle-list ManualObject. Same winding convention as ScaleGizmo's
// createCube.
void emitCube(Ogre::ManualObject* obj, const Ogre::Vector3& center, float s,
const Ogre::ColourValue& colour)
{
float cx = center.x, cy = center.y, cz = center.z;
int base = obj->getCurrentVertexCount();

auto vert = [&](float x, float y, float z) {
obj->position(Ogre::Vector3(x, y, z));
obj->colour(colour);
};
vert(cx - s, cy + s, cz + s);
vert(cx - s, cy - s, cz + s);
vert(cx + s, cy - s, cz + s);
vert(cx + s, cy + s, cz + s);
vert(cx - s, cy + s, cz - s);
vert(cx - s, cy - s, cz - s);
vert(cx + s, cy - s, cz - s);
vert(cx + s, cy + s, cz - s);

obj->quad(base + 0, base + 1, base + 2, base + 3);
obj->quad(base + 7, base + 6, base + 5, base + 4);
obj->quad(base + 0, base + 3, base + 7, base + 4);
obj->quad(base + 2, base + 1, base + 5, base + 6);
obj->quad(base + 3, base + 2, base + 6, base + 7);
obj->quad(base + 1, base + 0, base + 4, base + 5);
}
}

BevelGizmo::BevelGizmo(Ogre::SceneManager* sceneMgr, const Ogre::String& name)
: m_sceneMgr(sceneMgr)
{
if (!sceneMgr) return;

m_node = sceneMgr->getRootSceneNode()->createChildSceneNode(name + "_Node");
m_shaftNode = m_node->createChildSceneNode(name + "_Shaft");
m_handleNode = m_node->createChildSceneNode(name + "_Handle");

buildGeometry(name);

m_shaftNode->attachObject(m_shaft);
m_handleNode->attachObject(m_handle);
setVisible(false);
}

BevelGizmo::~BevelGizmo()
{
if (!m_sceneMgr) return;

if (m_handle) {
if (m_handleNode) m_handleNode->detachObject(m_handle);
m_sceneMgr->destroyManualObject(m_handle);
}
if (m_shaft) {
if (m_shaftNode) m_shaftNode->detachObject(m_shaft);
m_sceneMgr->destroyManualObject(m_shaft);
}
if (m_handleNode) {
m_handleNode->getParent()->removeChild(m_handleNode);
m_sceneMgr->destroySceneNode(m_handleNode);
}
if (m_shaftNode) {
m_shaftNode->getParent()->removeChild(m_shaftNode);
m_sceneMgr->destroySceneNode(m_shaftNode);
}
if (m_node) {
m_node->getParent()->removeChild(m_node);
m_sceneMgr->destroySceneNode(m_node);
}
}

void BevelGizmo::buildGeometry(const Ogre::String& name)
{
const Ogre::ColourValue shaftColour(0.1f, 0.9f, 0.4f, 1.0f);
const Ogre::ColourValue handleColour(1.0f, 0.85f, 0.1f, 1.0f); // yellow-orange — stands out against green
const float shaftLength = 0.4f;
const float handleHalfSize = 0.06f;

// Shaft: line list along +Y in local space (the parent node's rotation
// remaps this to the target axis in world space).
m_shaft = m_sceneMgr->createManualObject(name + "_Shaft");
m_shaft->setDynamic(false);
m_shaft->begin(GUI_MATERIAL_NAME, Ogre::RenderOperation::OT_LINE_LIST);
m_shaft->position(Ogre::Vector3::ZERO); m_shaft->colour(shaftColour);
m_shaft->position(Ogre::Vector3(0, shaftLength, 0)); m_shaft->colour(shaftColour);
m_shaft->end();
// Explicit bounds so scene queries / culling don't drop it.
m_shaft->setBoundingBox(Ogre::AxisAlignedBox(
-0.01f, 0.0f, -0.01f, 0.01f, shaftLength, 0.01f));
m_shaft->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_shaft->setQueryFlags(0); // shaft is not pickable; only the handle is

// Handle cube at tip. Query-flag-pickable; the controller asks
// isHandle(obj) to identify hits.
m_handle = m_sceneMgr->createManualObject(name + "_Handle_Geom");
m_handle->setDynamic(false);
m_handle->begin(GUI_MATERIAL_NAME, Ogre::RenderOperation::OT_TRIANGLE_LIST);
emitCube(m_handle, Ogre::Vector3::ZERO, handleHalfSize, handleColour);
m_handle->end();
m_handle->setBoundingBox(Ogre::AxisAlignedBox(
-handleHalfSize, -handleHalfSize, -handleHalfSize,
handleHalfSize, handleHalfSize, handleHalfSize));
m_handle->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_handle->setQueryFlags(0xFFFFFFFF);

// Place handle at top of shaft (in local Y space).
m_handleNode->setPosition(0, shaftLength, 0);
}

void BevelGizmo::setAxis(const Ogre::Vector3& origin, const Ogre::Vector3& axis)
{
m_origin = origin;
Ogre::Vector3 a = axis;
if (a.length() < 1e-6f)
a = Ogre::Vector3::UNIT_Y;
else
a.normalise();
m_axis = a;

if (!m_node) return;
m_node->setPosition(origin);
// Rotate local +Y to point along `m_axis`.
m_node->setOrientation(Ogre::Vector3::UNIT_Y.getRotationTo(m_axis));
}

void BevelGizmo::setHandleOffset(float offset)
{
if (m_handleNode) m_handleNode->setPosition(0, offset, 0);
// Stretch just the shaft node so the line's tip lands at the handle.
// Shaft geometry is authored at length 0.4 in local Y; scaling Y by
// offset/0.4 puts its tip at (0, offset, 0) in the parent's frame.
if (m_shaftNode) {
float yScale = (offset > 1e-6f) ? (offset / 0.4f) : 1e-6f;
m_shaftNode->setScale(1.0f, yScale, 1.0f);
}
}

void BevelGizmo::setVisible(bool visible)

Check warning on line 162 in src/BevelGizmo.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function should be declared "const".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ2rXSmfxyCD9RcEEjpf&open=AZ2rXSmfxyCD9RcEEjpf&pullRequest=295
{
if (m_node) m_node->setVisible(visible, true);
}

bool BevelGizmo::isVisible() const
{
// m_node has no directly attached objects; the shaft and handle are
// attached to their own child nodes. Query the ManualObjects themselves
// so the reported visibility matches the cascaded setVisible() call.
if (m_shaft && m_shaft->isVisible()) return true;
if (m_handle && m_handle->isVisible()) return true;
return false;
}

void BevelGizmo::setScale(float scale)
{
m_scale = (scale > 1e-6f) ? scale : 1.0f;
if (m_node) m_node->setScale(m_scale, m_scale, m_scale);
}

void BevelGizmo::updateScreenSpaceScale(const Ogre::Camera* camera)
{
if (!camera || !m_node) return;
// Distance from camera to gizmo origin. For perspective cameras this
// scales the gizmo's world size linearly with distance, which produces
// a constant angular (pixel) size. An arbitrary coefficient (0.12) was
// chosen so the gizmo looks similar in pixel size to the old fixed
// 0.1-length shaft when the camera is ~1 unit from the origin.
float dist = (camera->getDerivedPosition() - m_origin).length();
if (dist < 1e-4f) dist = 1e-4f;
float s = dist * 0.12f;
m_node->setScale(s, s, s);
m_scale = s;
}

bool BevelGizmo::isHandle(const Ogre::MovableObject* obj) const
{
return obj && obj == static_cast<const Ogre::MovableObject*>(m_handle);
}

float BevelGizmo::distanceAlongAxis(const Ogre::Ray& ray) const
{
// Closest-point projection between two lines: the gizmo axis (origin, m_axis)
// and the viewer ray. Returns the signed parameter t along the axis for the
// axis-line's closest point to the ray.
const Ogre::Vector3& p1 = m_origin;
const Ogre::Vector3& d1 = m_axis; // unit
const Ogre::Vector3& p2 = ray.getOrigin();
Ogre::Vector3 d2 = ray.getDirection(); // assume unit

Ogre::Vector3 r = p1 - p2;
float a = d1.dotProduct(d1);
float b = d1.dotProduct(d2);
float c = d2.dotProduct(d2);
float d = d1.dotProduct(r);
float e = d2.dotProduct(r);
float denom = a * c - b * b;
if (std::abs(denom) < 1e-8f)
return 0.0f; // parallel
// t = parameter along axis from p1 toward closest point to ray
return (b * e - c * d) / denom;
}
99 changes: 99 additions & 0 deletions src/BevelGizmo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
-----------------------------------------------------------------------------------
A QtMeshEditor file

Copyright (c) Fernando Tonon (https://github.com/fernandotonon)

The MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
-----------------------------------------------------------------------------------
*/

#ifndef BEVEL_GIZMO_H
#define BEVEL_GIZMO_H

#include <OgreVector.h>

namespace Ogre {
class SceneNode;
class SceneManager;
class ManualObject;
class Ray;
class MovableObject;
}

/**
* @brief Single-axis drag gizmo used to tune the bevel width interactively.
*
* Draws a short shaft with a cube handle at the top. Attached to the world
* root; its origin + axis direction are set by the controller so the shaft
* points along the averaged surface normal of the beveled region.
*
* The gizmo itself doesn't store width — it only knows about world-space
* position, axis, and scale. The controller calls distanceAlongAxis(ray)
* during drag to compute a new width from the ray-to-axis intersection.
*/
class BevelGizmo
{
public:
BevelGizmo(Ogre::SceneManager* sceneMgr, const Ogre::String& name = "BevelGizmo");

Check failure on line 48 in src/BevelGizmo.h

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add the "explicit" keyword to this constructor.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ2rXSmJxyCD9RcEEjpc&open=AZ2rXSmJxyCD9RcEEjpc&pullRequest=295
~BevelGizmo();

BevelGizmo(const BevelGizmo&) = delete;
BevelGizmo& operator=(const BevelGizmo&) = delete;

/// Position the gizmo in world space and align its shaft with `axis`.
void setAxis(const Ogre::Vector3& origin, const Ogre::Vector3& axis);

/// Slide the handle cube along the shaft by `offset` local units from
/// the shaft base. Used during drag so the visible handle tracks the
/// current bevel width.
void setHandleOffset(float offset);

/// Adjust the node's scale so the gizmo keeps a roughly constant pixel
/// footprint regardless of camera distance. Call each frame with the
/// active viewport's camera.
void updateScreenSpaceScale(const Ogre::Camera* camera);

/// Show/hide.
void setVisible(bool visible);
bool isVisible() const;

/// Scale the gizmo's visible size (useful for camera distance adjustment).
void setScale(float scale);

/// True if `obj` is this gizmo's handle (for picking hit-tests).
bool isHandle(const Ogre::MovableObject* obj) const;

/// Project `ray` onto the gizmo's axis line through `origin` and return
/// the signed distance along the axis from origin to the closest point.
/// Returns 0 if the ray is parallel to the axis.
float distanceAlongAxis(const Ogre::Ray& ray) const;

Ogre::Vector3 origin() const { return m_origin; }
Ogre::Vector3 axis() const { return m_axis; }

private:
Ogre::SceneManager* m_sceneMgr = nullptr;
Ogre::SceneNode* m_node = nullptr; ///< Position + orientation.
Ogre::SceneNode* m_shaftNode = nullptr; ///< Child whose Y-scale stretches the shaft.
Ogre::SceneNode* m_handleNode = nullptr;///< Child holding the picking cube.
Ogre::ManualObject* m_shaft = nullptr; ///< Line.
Ogre::ManualObject* m_handle = nullptr; ///< Cube at top (pickable).
Ogre::Vector3 m_origin = Ogre::Vector3::ZERO;
Ogre::Vector3 m_axis = Ogre::Vector3::UNIT_Y;
float m_scale = 1.0f;

void buildGeometry(const Ogre::String& name);
};

#endif // BEVEL_GIZMO_H
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ EditorViewport.cpp
RotationGizmo.cpp
TranslationGizmo.cpp
ScaleGizmo.cpp
BevelGizmo.cpp
TransformOperator.cpp
PrimitivesWidget.cpp
PrimitiveObject.cpp
Expand Down Expand Up @@ -94,6 +95,7 @@ EditorViewport.h
RotationGizmo.h
TranslationGizmo.h
ScaleGizmo.h
BevelGizmo.h
TransformOperator.h
PrimitivesWidget.h
PrimitiveObject.h
Expand Down
Loading
Loading