netradiant-custom/radiant/patchmanip.cpp

1180 lines
41 KiB
C++

/*
Copyright (C) 1999-2006 Id Software, Inc. and contributors.
For a list of contributors, see the accompanying CONTRIBUTORS file.
This file is part of GtkRadiant.
GtkRadiant is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
GtkRadiant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with GtkRadiant; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "patchmanip.h"
#include "debugging/debugging.h"
#include "iselection.h"
#include "ipatch.h"
#include "math/vector.h"
#include "math/aabb.h"
#include "generic/callback.h"
#include "gtkutil/menu.h"
#include "gtkutil/image.h"
#include "map.h"
#include "mainframe.h"
#include "commands.h"
#include "gtkmisc.h"
#include "gtkdlgs.h"
#include "texwindow.h"
#include "xywindow.h"
#include "select.h"
#include "patch.h"
#include "grid.h"
#include "patchdialog.h"
PatchCreator* g_patchCreator = 0;
void Scene_PatchConstructPrefab( scene::Graph& graph, const AABB aabb, const char* shader, EPatchPrefab eType, int axis, std::size_t width = 3, std::size_t height = 3, bool redisperse = false ){
Select_Delete();
GlobalSelectionSystem().setSelectedAll( false );
NodeSmartReference node( g_patchCreator->createPatch() );
Node_getTraversable( Map_FindOrInsertWorldspawn( g_map ) )->insert( node );
Patch* patch = Node_getPatch( node );
patch->SetShader( shader );
patch->ConstructPrefab( aabb, eType, axis, width, height );
if( redisperse ){
patch->Redisperse( COL );
patch->Redisperse( ROW );
}
patch->controlPointsChanged();
{
scene::Path patchpath( makeReference( GlobalSceneGraph().root() ) );
patchpath.push( makeReference( *Map_GetWorldspawn( g_map ) ) );
patchpath.push( makeReference( node.get() ) );
Instance_getSelectable( *graph.find( patchpath ) )->setSelected( true );
}
}
void Patch_makeCaps( Patch& patch, scene::Instance& instance, EPatchCap type, const char* shader ){
if ( ( type == EPatchCap::EndCap || type == EPatchCap::IEndCap )
&& patch.getWidth() != 5 ) {
globalErrorStream() << "cannot create end-cap - patch width != 5\n";
return;
}
if ( ( type == EPatchCap::Bevel || type == EPatchCap::IBevel )
&& patch.getWidth() != 3 && patch.getWidth() != 5 ) {
globalErrorStream() << "cannot create bevel-cap - patch width != 3\n";
return;
}
if ( type == EPatchCap::Cylinder
&& patch.getWidth() != 9 ) {
globalErrorStream() << "cannot create cylinder-cap - patch width != 9\n";
return;
}
{
NodeSmartReference cap( g_patchCreator->createPatch() );
Node_getTraversable( instance.path().parent() )->insert( cap );
patch.MakeCap( Node_getPatch( cap ), type, ROW, true );
Node_getPatch( cap )->SetShader( shader );
scene::Path path( instance.path() );
path.pop();
path.push( makeReference( cap.get() ) );
selectPath( path, true );
}
{
NodeSmartReference cap( g_patchCreator->createPatch() );
Node_getTraversable( instance.path().parent() )->insert( cap );
patch.MakeCap( Node_getPatch( cap ), type, ROW, false );
Node_getPatch( cap )->SetShader( shader );
scene::Path path( instance.path() );
path.pop();
path.push( makeReference( cap.get() ) );
selectPath( path, true );
}
}
typedef std::vector<scene::Instance*> InstanceVector;
class PatchStoreInstance
{
InstanceVector& m_instances;
public:
PatchStoreInstance( InstanceVector& instances ) : m_instances( instances ){
}
void operator()( PatchInstance& patch ) const {
m_instances.push_back( &patch );
}
};
void Scene_PatchDoCap_Selected( scene::Graph& graph, const char* shader, EPatchCap type ){
InstanceVector instances;
Scene_forEachVisibleSelectedPatchInstance( PatchStoreInstance( instances ) );
for ( auto i : instances )
{
Patch_makeCaps( *Node_getPatch( i->path().top() ), *i, type, shader );
}
}
void Patch_deform( Patch& patch, scene::Instance& instance, const int deform, const int axis ){
patch.undoSave();
for ( PatchControlIter i = patch.begin(); i != patch.end(); ++i ){
PatchControl& control = *i;
int randomNumber = int( deform * ( float( std::rand() ) / float( RAND_MAX ) ) );
control.m_vertex[ axis ] += randomNumber;
}
patch.controlPointsChanged();
}
void Scene_PatchDeform( scene::Graph& graph, const int deform, const int axis )
{
InstanceVector instances;
Scene_forEachVisibleSelectedPatchInstance( PatchStoreInstance( instances ) );
for ( auto i : instances )
{
Patch_deform( *Node_getPatch( i->path().top() ), *i, deform, axis );
}
}
void Patch_thicken( Patch& patch, scene::Instance& instance, const float thickness, bool seams, const int axis ){
const auto aabb_small = []( const AABB& aabb ){
return ( aabb.extents[0] < 0.01 && aabb.extents[1] < 0.01 ) ||
( aabb.extents[1] < 0.01 && aabb.extents[2] < 0.01 ) ||
( aabb.extents[0] < 0.01 && aabb.extents[2] < 0.01 );
};
// Create a new patch node
NodeSmartReference node( g_patchCreator->createPatch() );
// Insert the node into original's entity
Node_getTraversable( instance.path().parent() )->insert( node );
// Retrieve the contained patch from the node
Patch* targetPatch = Node_getPatch( node );
// Create the opposite patch with the given thickness = distance
bool no12 = true;
bool no34 = true;
targetPatch->createThickenedOpposite( patch, thickness, axis, no12, no34 );
{ // Now select the newly created patch
scene::Path path( instance.parent()->path() );
path.push( makeReference( node.get() ) );
selectPath( path, true );
}
if( seams && thickness != 0.0f ){
int i = no12? 2 : 0;
int iend = no34? 2 : 4;
// Now create the four walls
for ( ; i < iend; ++i ){
// Allocate new patch
NodeSmartReference node = NodeSmartReference( g_patchCreator->createPatch() );
// Insert each node into worldspawn
Node_getTraversable( instance.path().parent() )->insert( node );
// Retrieve the contained patch from the node
Patch* wallPatch = Node_getPatch( node );
// Create the wall patch by passing i as wallIndex
wallPatch->createThickenedWall( patch, *targetPatch, i );
if( aabb_small( wallPatch->localAABB() ) ){
//globalOutputStream() << "Thicken: Discarding degenerate patch.\n";
Node_getTraversable( instance.path().parent() )->erase( node );
}
else { // Now select the newly created patch
scene::Path path( instance.parent()->path() );
path.push( makeReference( node.get() ) );
selectPath( path, true );
}
}
}
// Invert the target patch so that it faces the opposite direction
targetPatch->InvertMatrix();
if( aabb_small( targetPatch->localAABB() ) ){
//globalOutputStream() << "Thicken: Discarding degenerate patch.\n";
Node_getTraversable( instance.path().parent() )->erase( node );
}
}
void Scene_PatchThicken( scene::Graph& graph, const int thickness, bool seams, const int axis )
{
InstanceVector instances;
Scene_forEachVisibleSelectedPatchInstance( PatchStoreInstance( instances ) );
for ( auto i : instances )
{
Patch_thicken( *Node_getPatch( i->path().top() ), *i, thickness, seams, axis );
}
}
Patch* Scene_GetUltimateSelectedVisiblePatch(){
if ( GlobalSelectionSystem().countSelected() != 0 ) {
scene::Node& node = GlobalSelectionSystem().ultimateSelected().path().top();
if ( node.visible() ) {
return Node_getPatch( node );
}
}
return 0;
}
void Scene_PatchCapTexture_Selected( scene::Graph& graph ){
Scene_forEachVisibleSelectedPatch( []( Patch& patch ){ patch.CapTexture(); } );
SceneChangeNotify();
}
void Scene_PatchProjectTexture_Selected( scene::Graph& graph, const texdef_t& texdef, const Vector3* direction ){
Scene_forEachVisibleSelectedPatch( [texdef, direction]( Patch& patch ){ patch.ProjectTexture( texdef, direction ); } );
SceneChangeNotify();
}
void Scene_PatchProjectTexture_Selected( scene::Graph& graph, const TextureProjection& projection, const Vector3& normal ){
Scene_forEachVisibleSelectedPatch( [projection, normal]( Patch& patch ){ patch.ProjectTexture( projection, normal ); } );
SceneChangeNotify();
}
void Scene_PatchFlipTexture_Selected( scene::Graph& graph, int axis ){
Scene_forEachVisibleSelectedPatch( [axis]( Patch& patch ){ patch.FlipTexture( axis ); } );
}
void Scene_PatchNaturalTexture_Selected( scene::Graph& graph ){
Scene_forEachVisibleSelectedPatch( []( Patch& patch ){ patch.NaturalTexture(); } );
SceneChangeNotify();
}
void Scene_PatchTileTexture_Selected( scene::Graph& graph, float s, float t ){
Scene_forEachVisibleSelectedPatch( [s, t]( Patch& patch ){ patch.SetTextureRepeat( s, t ); } );
SceneChangeNotify();
}
void Scene_PatchInsertRemove_Selected( scene::Graph& graph, bool bInsert, bool bColumn, bool bFirst ){
Scene_forEachVisibleSelectedPatch( [bInsert, bColumn, bFirst]( Patch& patch ){ patch.InsertRemove( bInsert, bColumn, bFirst ); } );
}
void Scene_PatchInvert_Selected( scene::Graph& graph ){
Scene_forEachVisibleSelectedPatch( []( Patch& patch ){ patch.InvertMatrix(); } );
}
void Scene_PatchRedisperse_Selected( scene::Graph& graph, EMatrixMajor major ){
Scene_forEachVisibleSelectedPatch( [major]( Patch& patch ){ patch.Redisperse( major ); } );
}
void Scene_PatchSmooth_Selected( scene::Graph& graph, EMatrixMajor major ){
Scene_forEachVisibleSelectedPatch( [major]( Patch& patch ){ patch.Smooth( major ); } );
}
void Scene_PatchTranspose_Selected( scene::Graph& graph ){
Scene_forEachVisibleSelectedPatch( []( Patch& patch ){ patch.TransposeMatrix(); } );
}
void Scene_PatchSetShader_Selected( scene::Graph& graph, const char* name ){
Scene_forEachVisibleSelectedPatch( [name]( Patch& patch ){ patch.SetShader( name ); } );
SceneChangeNotify();
}
void Scene_PatchGetShader_Selected( scene::Graph& graph, CopiedString& name ){
if ( Patch* patch = Scene_GetUltimateSelectedVisiblePatch() )
name = patch->GetShader();
}
class PatchSelectByShader
{
const char* m_name;
public:
inline PatchSelectByShader( const char* name )
: m_name( name ){
}
void operator()( PatchInstance& patch ) const {
if ( shader_equal( patch.getPatch().GetShader(), m_name ) ) {
patch.setSelected( true );
}
}
};
void Scene_PatchSelectByShader( scene::Graph& graph, const char* name ){
Scene_forEachVisiblePatchInstance( PatchSelectByShader( name ) );
}
class PatchFindReplaceShader
{
const char* m_find;
const char* m_replace;
public:
PatchFindReplaceShader( const char* find, const char* replace ) : m_find( find ), m_replace( replace ){
}
void operator()( Patch& patch ) const {
if ( shader_equal( patch.GetShader(), m_find ) ) {
patch.SetShader( m_replace );
}
}
};
void Scene_PatchFindReplaceShader( scene::Graph& graph, const char* find, const char* replace ){
if( !replace ){
Scene_forEachVisiblePatchInstance( PatchSelectByShader( find ) );
}
else{
Scene_forEachVisiblePatch( PatchFindReplaceShader( find, replace ) );
}
}
void Scene_PatchFindReplaceShader_Selected( scene::Graph& graph, const char* find, const char* replace ){
if( !replace ){
//do nothing, because alternative is replacing to notex
//perhaps deselect ones with not matching shaders here?
}
else{
Scene_forEachVisibleSelectedPatch( PatchFindReplaceShader( find, replace ) );
}
}
AABB PatchCreator_getBounds(){
AABB aabb( aabb_for_minmax( Select_getWorkZone().d_work_min, Select_getWorkZone().d_work_max ) );
float gridSize = GetGridSize();
if ( aabb.extents[0] == 0 ) {
aabb.extents[0] = gridSize;
}
if ( aabb.extents[1] == 0 ) {
aabb.extents[1] = gridSize;
}
if ( aabb.extents[2] == 0 ) {
aabb.extents[2] = gridSize;
}
if ( aabb_valid( aabb ) ) {
return aabb;
}
return AABB( Vector3( 0, 0, 0 ), Vector3( 64, 64, 64 ) );
}
void DoNewPatchDlg( EPatchPrefab prefab, int minrows, int mincols, int defrows, int defcols, int maxrows, int maxcols );
void Patch_XactCylinder(){
UndoableCommand undo( "patchCreateXactCylinder" );
DoNewPatchDlg( EPatchPrefab::ExactCylinder, 3, 7, 3, 13, 0, 0 );
}
void Patch_XactSphere(){
UndoableCommand undo( "patchCreateXactSphere" );
DoNewPatchDlg( EPatchPrefab::ExactSphere, 5, 7, 7, 13, 0, 0 );
}
void Patch_XactCone(){
UndoableCommand undo( "patchCreateXactCone" );
DoNewPatchDlg( EPatchPrefab::ExactCone, 3, 7, 3, 13, 0, 0 );
}
void Patch_Cylinder(){
UndoableCommand undo( "patchCreateCylinder" );
Scene_PatchConstructPrefab( GlobalSceneGraph(), PatchCreator_getBounds(), TextureBrowser_GetSelectedShader(), EPatchPrefab::Cylinder, GlobalXYWnd_getCurrentViewType() );
}
void Patch_DenseCylinder(){
UndoableCommand undo( "patchCreateDenseCylinder" );
Scene_PatchConstructPrefab( GlobalSceneGraph(), PatchCreator_getBounds(), TextureBrowser_GetSelectedShader(), EPatchPrefab::DenseCylinder, GlobalXYWnd_getCurrentViewType() );
}
void Patch_VeryDenseCylinder(){
UndoableCommand undo( "patchCreateVeryDenseCylinder" );
Scene_PatchConstructPrefab( GlobalSceneGraph(), PatchCreator_getBounds(), TextureBrowser_GetSelectedShader(), EPatchPrefab::VeryDenseCylinder, GlobalXYWnd_getCurrentViewType() );
}
void Patch_SquareCylinder(){
UndoableCommand undo( "patchCreateSquareCylinder" );
Scene_PatchConstructPrefab( GlobalSceneGraph(), PatchCreator_getBounds(), TextureBrowser_GetSelectedShader(), EPatchPrefab::SqCylinder, GlobalXYWnd_getCurrentViewType() );
}
void Patch_Endcap(){
UndoableCommand undo( "patchCreateEndCap" );
Scene_PatchConstructPrefab( GlobalSceneGraph(), PatchCreator_getBounds(), TextureBrowser_GetSelectedShader(), EPatchPrefab::EndCap, GlobalXYWnd_getCurrentViewType() );
}
void Patch_Bevel(){
UndoableCommand undo( "patchCreateBevel" );
Scene_PatchConstructPrefab( GlobalSceneGraph(), PatchCreator_getBounds(), TextureBrowser_GetSelectedShader(), EPatchPrefab::Bevel, GlobalXYWnd_getCurrentViewType() );
}
void Patch_Sphere(){
UndoableCommand undo( "patchCreateSphere" );
Scene_PatchConstructPrefab( GlobalSceneGraph(), PatchCreator_getBounds(), TextureBrowser_GetSelectedShader(), EPatchPrefab::Sphere, GlobalXYWnd_getCurrentViewType() );
}
void Patch_SquareBevel(){
}
void Patch_SquareEndcap(){
}
void Patch_Cone(){
UndoableCommand undo( "patchCreateCone" );
Scene_PatchConstructPrefab( GlobalSceneGraph(), PatchCreator_getBounds(), TextureBrowser_GetSelectedShader(), EPatchPrefab::Cone, GlobalXYWnd_getCurrentViewType() );
}
void Patch_Plane(){
UndoableCommand undo( "patchCreatePlane" );
DoNewPatchDlg( EPatchPrefab::Plane, 3, 3, 3, 3, 0, 0 );
}
void Patch_InsertFirstColumn(){
UndoableCommand undo( "patchInsertFirstColumns" );
Scene_PatchInsertRemove_Selected( GlobalSceneGraph(), true, true, true );
}
void Patch_InsertLastColumn(){
UndoableCommand undo( "patchInsertLastColumns" );
Scene_PatchInsertRemove_Selected( GlobalSceneGraph(), true, true, false );
}
void Patch_InsertFirstRow(){
UndoableCommand undo( "patchInsertFirstRows" );
Scene_PatchInsertRemove_Selected( GlobalSceneGraph(), true, false, true );
}
void Patch_InsertLastRow(){
UndoableCommand undo( "patchInsertLastRows" );
Scene_PatchInsertRemove_Selected( GlobalSceneGraph(), true, false, false );
}
void Patch_DeleteFirstColumn(){
UndoableCommand undo( "patchDeleteFirstColumns" );
Scene_PatchInsertRemove_Selected( GlobalSceneGraph(), false, true, true );
}
void Patch_DeleteLastColumn(){
UndoableCommand undo( "patchDeleteLastColumns" );
Scene_PatchInsertRemove_Selected( GlobalSceneGraph(), false, true, false );
}
void Patch_DeleteFirstRow(){
UndoableCommand undo( "patchDeleteFirstRows" );
Scene_PatchInsertRemove_Selected( GlobalSceneGraph(), false, false, true );
}
void Patch_DeleteLastRow(){
UndoableCommand undo( "patchDeleteLastRows" );
Scene_PatchInsertRemove_Selected( GlobalSceneGraph(), false, false, false );
}
void Patch_Invert(){
UndoableCommand undo( "patchInvert" );
Scene_PatchInvert_Selected( GlobalSceneGraph() );
}
void Patch_RedisperseRows(){
UndoableCommand undo( "patchRedisperseRows" );
Scene_PatchRedisperse_Selected( GlobalSceneGraph(), ROW );
}
void Patch_RedisperseCols(){
UndoableCommand undo( "patchRedisperseColumns" );
Scene_PatchRedisperse_Selected( GlobalSceneGraph(), COL );
}
void Patch_SmoothRows(){
UndoableCommand undo( "patchSmoothRows" );
Scene_PatchSmooth_Selected( GlobalSceneGraph(), ROW );
}
void Patch_SmoothCols(){
UndoableCommand undo( "patchSmoothColumns" );
Scene_PatchSmooth_Selected( GlobalSceneGraph(), COL );
}
void Patch_Transpose(){
UndoableCommand undo( "patchTranspose" );
Scene_PatchTranspose_Selected( GlobalSceneGraph() );
}
void DoCapDlg();
void Patch_Cap(){
// FIXME: add support for patch cap creation
// Patch_CapCurrent();
UndoableCommand undo( "patchPutCaps" );
DoCapDlg();
}
///\todo Unfinished.
void Patch_OverlayOn(){
}
///\todo Unfinished.
void Patch_OverlayOff(){
}
void Patch_FlipTextureX(){
UndoableCommand undo( "patchFlipTextureU" );
Scene_PatchFlipTexture_Selected( GlobalSceneGraph(), 0 );
}
void Patch_FlipTextureY(){
UndoableCommand undo( "patchFlipTextureV" );
Scene_PatchFlipTexture_Selected( GlobalSceneGraph(), 1 );
}
void Patch_NaturalTexture(){
UndoableCommand undo( "patchNaturalTexture" );
Scene_PatchNaturalTexture_Selected( GlobalSceneGraph() );
}
void Patch_CapTexture(){
UndoableCommand command( "patchCapTexture" );
Scene_PatchCapTexture_Selected( GlobalSceneGraph() );
}
void Patch_FitTexture(){
}
void DoPatchDeformDlg();
void Patch_Deform(){
UndoableCommand undo( "patchDeform" );
DoPatchDeformDlg();
}
void DoPatchThickenDlg();
void Patch_Thicken(){
UndoableCommand undo( "patchThicken" );
DoPatchThickenDlg();
}
#include "ifilter.h"
class filter_patch_all : public PatchFilter
{
public:
bool filter( const Patch& patch ) const {
return true;
}
};
class filter_patch_shader : public PatchFilter
{
const char* m_shader;
public:
filter_patch_shader( const char* shader ) : m_shader( shader ){
}
bool filter( const Patch& patch ) const {
return shader_equal( patch.GetShader(), m_shader );
}
};
class filter_patch_flags : public PatchFilter
{
int m_flags;
public:
filter_patch_flags( int flags ) : m_flags( flags ){
}
bool filter( const Patch& patch ) const {
return ( patch.getShaderFlags() & m_flags ) != 0;
}
};
filter_patch_all g_filter_patch_all;
filter_patch_flags g_filter_patch_clip( QER_CLIP );
filter_patch_shader g_filter_patch_commonclip( "textures/common/clip" );
filter_patch_shader g_filter_patch_weapclip( "textures/common/weapclip" );
filter_patch_flags g_filter_patch_translucent( QER_TRANS | QER_ALPHATEST );
void PatchFilters_construct(){
add_patch_filter( g_filter_patch_all, EXCLUDE_CURVES );
add_patch_filter( g_filter_patch_clip, EXCLUDE_CLIP );
add_patch_filter( g_filter_patch_commonclip, EXCLUDE_CLIP );
add_patch_filter( g_filter_patch_weapclip, EXCLUDE_CLIP );
add_patch_filter( g_filter_patch_translucent, EXCLUDE_TRANSLUCENT );
}
#include "preferences.h"
void Patch_constructPreferences( PreferencesPage& page ){
page.appendSpinner( "Patch Subdivide Threshold", g_PatchSubdivideThreshold, 0, 128 );
}
void Patch_constructPage( PreferenceGroup& group ){
PreferencesPage page( group.createPage( "Patches", "Patch Display Preferences" ) );
Patch_constructPreferences( page );
}
void Patch_registerPreferencesPage(){
PreferencesDialog_addDisplayPage( FreeCaller1<PreferenceGroup&, Patch_constructPage>() );
}
#include "preferencesystem.h"
void PatchPreferences_construct(){
GlobalPreferenceSystem().registerPreference( "Subdivisions", IntImportStringCaller( g_PatchSubdivideThreshold ), IntExportStringCaller( g_PatchSubdivideThreshold ) );
}
#include "generic/callback.h"
void Patch_registerCommands(){
GlobalCommands_insert( "InvertCurveTextureX", FreeCaller<Patch_FlipTextureX>(), QKeySequence( "Ctrl+Shift+I" ) );
GlobalCommands_insert( "InvertCurveTextureY", FreeCaller<Patch_FlipTextureY>(), QKeySequence( "Shift+I" ) );
GlobalCommands_insert( "NaturalizePatch", FreeCaller<Patch_NaturalTexture>(), QKeySequence( "Ctrl+N" ) );
GlobalCommands_insert( "PatchCylinder", FreeCaller<Patch_Cylinder>() );
// GlobalCommands_insert( "PatchDenseCylinder", FreeCaller<Patch_DenseCylinder>() );
// GlobalCommands_insert( "PatchVeryDenseCylinder", FreeCaller<Patch_VeryDenseCylinder>() );
GlobalCommands_insert( "PatchSquareCylinder", FreeCaller<Patch_SquareCylinder>() );
GlobalCommands_insert( "PatchXactCylinder", FreeCaller<Patch_XactCylinder>() );
GlobalCommands_insert( "PatchXactSphere", FreeCaller<Patch_XactSphere>() );
GlobalCommands_insert( "PatchXactCone", FreeCaller<Patch_XactCone>() );
GlobalCommands_insert( "PatchEndCap", FreeCaller<Patch_Endcap>() );
GlobalCommands_insert( "PatchBevel", FreeCaller<Patch_Bevel>() );
// GlobalCommands_insert( "PatchSquareBevel", FreeCaller<Patch_SquareBevel>() );
// GlobalCommands_insert( "PatchSquareEndcap", FreeCaller<Patch_SquareEndcap>() );
GlobalCommands_insert( "PatchCone", FreeCaller<Patch_Cone>() );
GlobalCommands_insert( "PatchSphere", FreeCaller<Patch_Sphere>() );
GlobalCommands_insert( "SimplePatchMesh", FreeCaller<Patch_Plane>(), QKeySequence( "Shift+P" ) );
GlobalCommands_insert( "PatchInsertFirstColumn", FreeCaller<Patch_InsertFirstColumn>(), QKeySequence( Qt::CTRL + Qt::SHIFT + Qt::Key_Plus + Qt::KeypadModifier ) );
GlobalCommands_insert( "PatchInsertLastColumn", FreeCaller<Patch_InsertLastColumn>() );
GlobalCommands_insert( "PatchInsertFirstRow", FreeCaller<Patch_InsertFirstRow>(), QKeySequence( Qt::CTRL + Qt::Key_Plus + Qt::KeypadModifier ) );
GlobalCommands_insert( "PatchInsertLastRow", FreeCaller<Patch_InsertLastRow>() );
GlobalCommands_insert( "PatchDeleteFirstColumn", FreeCaller<Patch_DeleteFirstColumn>() );
GlobalCommands_insert( "PatchDeleteLastColumn", FreeCaller<Patch_DeleteLastColumn>(), QKeySequence( Qt::CTRL + Qt::SHIFT + Qt::Key_Minus + Qt::KeypadModifier ) );
GlobalCommands_insert( "PatchDeleteFirstRow", FreeCaller<Patch_DeleteFirstRow>() );
GlobalCommands_insert( "PatchDeleteLastRow", FreeCaller<Patch_DeleteLastRow>(), QKeySequence( Qt::CTRL + Qt::Key_Minus + Qt::KeypadModifier ) );
GlobalCommands_insert( "InvertCurve", FreeCaller<Patch_Invert>(), QKeySequence( "Ctrl+I" ) );
//GlobalCommands_insert( "RedisperseRows", FreeCaller<Patch_RedisperseRows>(), QKeySequence( "Ctrl+E" ) );
GlobalCommands_insert( "RedisperseRows", FreeCaller<Patch_RedisperseRows>() );
//GlobalCommands_insert( "RedisperseCols", FreeCaller<Patch_RedisperseCols>(), QKeySequence( "Ctrl+Shift+E" ) );
GlobalCommands_insert( "RedisperseCols", FreeCaller<Patch_RedisperseCols>() );
GlobalCommands_insert( "SmoothRows", FreeCaller<Patch_SmoothRows>(), QKeySequence( "Ctrl+W" ) );
GlobalCommands_insert( "SmoothCols", FreeCaller<Patch_SmoothCols>(), QKeySequence( "Ctrl+Shift+W" ) );
GlobalCommands_insert( "MatrixTranspose", FreeCaller<Patch_Transpose>(), QKeySequence( "Ctrl+Shift+M" ) );
GlobalCommands_insert( "CapCurrentCurve", FreeCaller<Patch_Cap>(), QKeySequence( "Shift+C" ) );
// GlobalCommands_insert( "MakeOverlayPatch", FreeCaller<Patch_OverlayOn>(), QKeySequence( "Y" ) );
// GlobalCommands_insert( "ClearPatchOverlays", FreeCaller<Patch_OverlayOff>(), QKeySequence( "Ctrl+L" ) );
GlobalCommands_insert( "PatchDeform", FreeCaller<Patch_Deform>() );
GlobalCommands_insert( "PatchThicken", FreeCaller<Patch_Thicken>(), QKeySequence( "Ctrl+T" ) );
}
void Patch_constructToolbar( QToolBar* toolbar ){
toolbar_append_button( toolbar, "Put caps on the current patch", "curve_cap.png", "CapCurrentCurve" );
}
void Patch_constructMenu( QMenu* menu ){
create_menu_item_with_mnemonic( menu, "Simple Patch Mesh...", "SimplePatchMesh" );
create_menu_item_with_mnemonic( menu, "Bevel", "PatchBevel" );
create_menu_item_with_mnemonic( menu, "End cap", "PatchEndCap" );
create_menu_item_with_mnemonic( menu, "Cylinder (9x3)", "PatchCylinder" );
create_menu_item_with_mnemonic( menu, "Square Cylinder (9x3)", "PatchSquareCylinder" );
create_menu_item_with_mnemonic( menu, "Exact Cylinder...", "PatchXactCylinder" );
create_menu_item_with_mnemonic( menu, "Cone (9x3)", "PatchCone" );
create_menu_item_with_mnemonic( menu, "Exact Cone...", "PatchXactCone" );
create_menu_item_with_mnemonic( menu, "Sphere (9x5)", "PatchSphere" );
create_menu_item_with_mnemonic( menu, "Exact Sphere...", "PatchXactSphere" );
// {
// QMenu* submenu = menu->addMenu( "More Cylinders" );
// submenu->setTearOffEnabled( g_Layout_enableDetachableMenus.m_value );
// create_menu_item_with_mnemonic( submenu, "Dense Cylinder", "PatchDenseCylinder" );
// create_menu_item_with_mnemonic( submenu, "Very Dense Cylinder", "PatchVeryDenseCylinder" );
// create_menu_item_with_mnemonic( submenu, "Square Cylinder", "PatchSquareCylinder" );
// }
// {
// //not implemented
// create_menu_item_with_mnemonic( menu, "Square Endcap", "PatchSquareBevel" );
// create_menu_item_with_mnemonic( menu, "Square Bevel", "PatchSquareEndcap" );
// }
menu->addSeparator();
create_menu_item_with_mnemonic( menu, "Cap Selection", "CapCurrentCurve" );
menu->addSeparator();
{
QMenu* submenu = menu->addMenu( "Insert/Delete" );
submenu->setTearOffEnabled( g_Layout_enableDetachableMenus.m_value );
create_menu_item_with_mnemonic( submenu, "Insert (2) First Columns", "PatchInsertFirstColumn" );
create_menu_item_with_mnemonic( submenu, "Insert (2) Last Columns", "PatchInsertLastColumn" );
submenu->addSeparator();
create_menu_item_with_mnemonic( submenu, "Insert (2) First Rows", "PatchInsertFirstRow" );
create_menu_item_with_mnemonic( submenu, "Insert (2) Last Rows", "PatchInsertLastRow" );
submenu->addSeparator();
create_menu_item_with_mnemonic( submenu, "Del First (2) Columns", "PatchDeleteFirstColumn" );
create_menu_item_with_mnemonic( submenu, "Del Last (2) Columns", "PatchDeleteLastColumn" );
submenu->addSeparator();
create_menu_item_with_mnemonic( submenu, "Del First (2) Rows", "PatchDeleteFirstRow" );
create_menu_item_with_mnemonic( submenu, "Del Last (2) Rows", "PatchDeleteLastRow" );
}
menu->addSeparator();
{
QMenu* submenu = menu->addMenu( "Matrix" );
submenu->setTearOffEnabled( g_Layout_enableDetachableMenus.m_value );
create_menu_item_with_mnemonic( submenu, "Invert", "InvertCurve" );
create_menu_item_with_mnemonic( submenu, "Transpose", "MatrixTranspose" );
submenu->addSeparator();
create_menu_item_with_mnemonic( submenu, "Re-disperse Rows", "RedisperseRows" );
create_menu_item_with_mnemonic( submenu, "Re-disperse Columns", "RedisperseCols" );
submenu->addSeparator();
create_menu_item_with_mnemonic( submenu, "Smooth Rows", "SmoothRows" );
create_menu_item_with_mnemonic( submenu, "Smooth Columns", "SmoothCols" );
}
menu->addSeparator();
{
QMenu* submenu = menu->addMenu( "Texture" );
submenu->setTearOffEnabled( g_Layout_enableDetachableMenus.m_value );
create_menu_item_with_mnemonic( submenu, "Reset Texture", "TextureReset/Cap" );
create_menu_item_with_mnemonic( submenu, "Naturalize", "NaturalizePatch" );
create_menu_item_with_mnemonic( submenu, "Invert X", "InvertCurveTextureX" );
create_menu_item_with_mnemonic( submenu, "Invert Y", "InvertCurveTextureY" );
}
// menu->addSeparator();
// { //unfinished
// QMenu* submenu = menu->addMenu( "Overlay" );
// submenu->setTearOffEnabled( g_Layout_enableDetachableMenus.m_value );
// create_menu_item_with_mnemonic( submenu, "Set", "MakeOverlayPatch" );
// create_menu_item_with_mnemonic( submenu, "Clear", "ClearPatchOverlays" );
// }
menu->addSeparator();
create_menu_item_with_mnemonic( menu, "Deform...", "PatchDeform" );
create_menu_item_with_mnemonic( menu, "Thicken...", "PatchThicken" );
}
#include "gtkutil/dialog.h"
#include "gtkutil/widget.h"
#include "gtkutil/spinbox.h"
#include <QDialog>
#include "gtkutil/combobox.h"
#include <QCheckBox>
#include <QFormLayout>
#include <QDialogButtonBox>
#include <QButtonGroup>
#include <QRadioButton>
void DoNewPatchDlg( EPatchPrefab prefab, int minrows, int mincols, int defrows, int defcols, int maxrows, int maxcols ){
QDialog dialog( MainFrame_getWindow(), Qt::Dialog | Qt::WindowCloseButtonHint );
dialog.setWindowTitle( "Patch density" );
auto width = new ComboBox;
auto height = new ComboBox;
auto redisperseCheckBox = new QCheckBox( "Square" );
{
auto form = new QFormLayout( &dialog );
form->setSizeConstraint( QLayout::SizeConstraint::SetFixedSize );
{
{
#define D_ITEM( x ) if ( x >= mincols && ( !maxcols || x <= maxcols ) ) width->addItem( # x )
D_ITEM( 3 );
D_ITEM( 5 );
D_ITEM( 7 );
D_ITEM( 9 );
D_ITEM( 11 );
D_ITEM( 13 );
D_ITEM( 15 );
D_ITEM( 17 );
D_ITEM( 19 );
D_ITEM( 21 );
D_ITEM( 23 );
D_ITEM( 25 );
D_ITEM( 27 );
D_ITEM( 29 );
D_ITEM( 31 ); // MAX_PATCH_SIZE is 32, so we should be able to do 31...
#undef D_ITEM
form->addRow( "Width:", width );
}
{
#define D_ITEM( x ) if ( x >= minrows && ( !maxrows || x <= maxrows ) ) height->addItem( # x )
D_ITEM( 3 );
D_ITEM( 5 );
D_ITEM( 7 );
D_ITEM( 9 );
D_ITEM( 11 );
D_ITEM( 13 );
D_ITEM( 15 );
D_ITEM( 17 );
D_ITEM( 19 );
D_ITEM( 21 );
D_ITEM( 23 );
D_ITEM( 25 );
D_ITEM( 27 );
D_ITEM( 29 );
D_ITEM( 31 ); // MAX_PATCH_SIZE is 32, so we should be able to do 31...
#undef D_ITEM
form->addRow( "Height:", height );
}
if( prefab != EPatchPrefab::Plane ){
redisperseCheckBox->setToolTip( "Redisperse columns & rows" );
form->addWidget( redisperseCheckBox );
}
}
{
auto buttons = new QDialogButtonBox( QDialogButtonBox::StandardButton::Ok | QDialogButtonBox::StandardButton::Cancel );
form->addWidget( buttons );
QObject::connect( buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept );
QObject::connect( buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject );
}
}
// Initialize dialog
width->setCurrentIndex( ( defcols - mincols ) / 2 );
height->setCurrentIndex( ( defrows - minrows ) / 2 );
if ( dialog.exec() ) {
const int w = width->currentIndex() * 2 + mincols;
const int h = height->currentIndex() * 2 + minrows;
const bool redisperse = redisperseCheckBox->isChecked();
Scene_PatchConstructPrefab( GlobalSceneGraph(), PatchCreator_getBounds(), TextureBrowser_GetSelectedShader(), prefab, GlobalXYWnd_getCurrentViewType(), w, h, redisperse );
}
}
void DoPatchDeformDlg(){
QDialog dialog( MainFrame_getWindow(), Qt::Dialog | Qt::WindowCloseButtonHint );
dialog.setWindowTitle( "Patch deform" );
auto spin = new SpinBox( -9999, 9999, 64 );
RadioHBox radioBox = RadioHBox_new( (const char*[]){ "X", "Y", "Z" } );
radioBox.m_radio->button( 2 )->setChecked( true );
{
auto form = new QFormLayout( &dialog );
form->setSizeConstraint( QLayout::SizeConstraint::SetFixedSize );
form->addRow( new SpinBoxLabel( "Max deform:", spin ), spin );
form->addRow( "", radioBox.m_hbox );
{
auto buttons = new QDialogButtonBox( QDialogButtonBox::StandardButton::Ok | QDialogButtonBox::StandardButton::Cancel );
form->addWidget( buttons );
QObject::connect( buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept );
QObject::connect( buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject );
}
}
if ( dialog.exec() ) {
const int deform = spin->value();
const int axis = radioBox.m_radio->checkedId();
Scene_PatchDeform( GlobalSceneGraph(), deform, axis );
}
}
void DoCapDlg(){
QDialog dialog( MainFrame_getWindow(), Qt::Dialog | Qt::WindowCloseButtonHint );
dialog.setWindowTitle( "Cap" );
auto group = new QButtonGroup( &dialog );
{
auto form = new QFormLayout( &dialog );
form->setSizeConstraint( QLayout::SizeConstraint::SetFixedSize );
{
const char* iconlabel[][2] = { { "cap_bevel.png", "Bevel" },
{ "cap_endcap.png", "Endcap" },
{ "cap_ibevel.png", "Inverted Bevel" },
{ "cap_iendcap.png", "Inverted Endcap" },
{ "cap_cylinder.png", "Cylinder" } };
for( size_t i = 0; i < std::size( iconlabel ); ++i ){
const auto [ stricon, strlabel ] = iconlabel[i];
auto label = new QLabel;
label->setPixmap( new_local_image( stricon ) );
auto button = new QRadioButton( strlabel );
group->addButton( button, i ); // set ids 0+, default ones are negative
form->addRow( label, button );
}
for( int i = 0; i < form->count(); ++i )
form->itemAt( i )->setAlignment( Qt::AlignmentFlag::AlignVCenter );
}
{
auto buttons = new QDialogButtonBox( QDialogButtonBox::StandardButton::Ok | QDialogButtonBox::StandardButton::Cancel );
form->addWidget( buttons );
QObject::connect( buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept );
QObject::connect( buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject );
}
}
// Initialize dialog
group->button( 0 )->setChecked( true );
if( dialog.exec() ){
Scene_PatchDoCap_Selected( GlobalSceneGraph(), TextureBrowser_GetSelectedShader(), static_cast<EPatchCap>( group->checkedId() ) );
}
}
void DoPatchThickenDlg(){
QDialog dialog( MainFrame_getWindow(), Qt::Dialog | Qt::WindowCloseButtonHint );
dialog.setWindowTitle( "Patch thicken" );
const int grid = std::max( GetGridSize(), 1.f );
auto spin = new SpinBox( -9999, 9999, grid, 2, grid );
RadioHBox radioBox = RadioHBox_new( (const char*[]){ "X", "Y", "Z", "Normal" } );
radioBox.m_radio->button( 3 )->setChecked( true );
auto check = new QCheckBox( "Side walls" );
check->setChecked( true );
{
auto form = new QFormLayout( &dialog );
form->setSizeConstraint( QLayout::SizeConstraint::SetFixedSize );
form->addRow( new SpinBoxLabel( "Thickness:", spin ), spin );
form->addRow( "", radioBox.m_hbox );
form->addWidget( check );
{
auto buttons = new QDialogButtonBox( QDialogButtonBox::StandardButton::Ok | QDialogButtonBox::StandardButton::Cancel );
form->addWidget( buttons );
QObject::connect( buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept );
QObject::connect( buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject );
}
}
if ( dialog.exec() ) {
const int thickness = spin->value();
const bool seams = check->isChecked();
const int axis = radioBox.m_radio->checkedId(); // 3 == Extrude along normals
Scene_PatchThicken( GlobalSceneGraph(), thickness, seams, axis );
}
}
class PatchTexdefConstructor
{
public:
brushprimit_texdef_t m_bp;
Matrix4 m_local2tex;
Matrix4 m_tex2local;
Plane3 m_plane;
// rip from UVManipulator::UpdateFaceData
PatchTexdefConstructor( Patch *patch ){
m_plane.normal() = patch->Calculate_AvgNormal();
m_plane.dist() = vector3_dot( m_plane.normal(), patch->localAABB().origin );
const size_t patchWidth = patch->getWidth();
const size_t patchHeight = patch->getHeight();
{ //! todo force or deduce orthogonal uv axes for convenience
Vector3 wDir, hDir;
patch->Calculate_AvgAxes( wDir, hDir );
vector3_normalise( wDir );
vector3_normalise( hDir );
// globalOutputStream() << wDir << " wDir\n";
// globalOutputStream() << hDir << " hDir\n";
// globalOutputStream() << m_plane.normal() << " m_plane.normal()\n";
/* find longest row and column */
float wLength = 0, hLength = 0; //!? todo break, if some of these is 0
std::size_t row = 0, col = 0;
for ( std::size_t r = 0; r < patchHeight; ++r ){
float length = 0;
for ( std::size_t c = 0; c < patchWidth - 1; ++c ){
length += vector3_length( patch->ctrlAt( r, c + 1 ).m_vertex - patch->ctrlAt( r, c ).m_vertex );
}
if( length - wLength > .1f || ( ( r == 0 || r == patchHeight - 1 ) && float_equal_epsilon( length, wLength, .1f ) ) ){ // prioritize first and last rows
wLength = length;
row = r;
}
}
for ( std::size_t c = 0; c < patchWidth; ++c ){
float length = 0;
for ( std::size_t r = 0; r < patchHeight - 1; ++r ){
length += vector3_length( patch->ctrlAt( r + 1, c ).m_vertex - patch->ctrlAt( r, c ).m_vertex );
}
if( length - hLength > .1f || ( ( c == 0 || c == patchWidth - 1 ) && float_equal_epsilon( length, hLength, .1f ) ) ){
hLength = length;
col = c;
}
}
//! todo handle case, when uv start = end, like projection to cylinder
//! todo consider max uv length to have manipulator size according to patch size
/* pick 3 points at the found row and column */
const PatchControl* p0, *p1, *p2;
Vector3 v0, v1, v2;
{
float distW0 = 0, distW1 = 0;
for ( std::size_t c = 0; c < col; ++c ){
distW0 += vector3_length( patch->ctrlAt( row, c + 1 ).m_vertex - patch->ctrlAt( row, c ).m_vertex );
}
for ( std::size_t c = col; c < patchWidth - 1; ++c ){
distW1 += vector3_length( patch->ctrlAt( row, c + 1 ).m_vertex - patch->ctrlAt( row, c ).m_vertex );
}
float distH0 = 0, distH1 = 0;
for ( std::size_t r = 0; r < row; ++r ){
distH0 += vector3_length( patch->ctrlAt( r + 1, col ).m_vertex - patch->ctrlAt( r, col ).m_vertex );
}
for ( std::size_t r = row; r < patchHeight - 1; ++r ){
distH1 += vector3_length( patch->ctrlAt( r + 1, col ).m_vertex - patch->ctrlAt( r, col ).m_vertex );
}
if( ( distW0 > distH0 && distW0 > distH1 ) || ( distW1 > distH0 && distW1 > distH1 ) ){
p0 = &patch->ctrlAt( 0, col );
p1 = &patch->ctrlAt( patchHeight - 1, col );
p2 = distW0 > distW1? &patch->ctrlAt( row, 0 ) : &patch->ctrlAt( row, patchWidth - 1 );
v0 = p0->m_vertex; //! the altered line, we want realistic offset values
v1 = v0 + hDir * hLength;
v2 = v0 + hDir * distH0 + ( distW0 > distW1? ( wDir * -distW0 ) : ( wDir * distW1 ) );
}
else{
p0 = &patch->ctrlAt( row, 0 );
p1 = &patch->ctrlAt( row, patchWidth - 1 );
p2 = distH0 > distH1? &patch->ctrlAt( 0, col ) : &patch->ctrlAt( patchHeight - 1, col );
v0 = p0->m_vertex; //! the altered line, we want realistic offset values
v1 = v0 + wDir * wLength;
v2 = v0 + wDir * distW0 + ( distH0 > distH1? ( hDir * -distH0 ) : ( hDir * distH1 ) );
}
if( vector3_dot( plane3_for_points( v0, v1, v2 ).normal(), m_plane.normal() ) < 0 ){
std::swap( p0, p1 );
std::swap( v0, v1 );
}
}
const DoubleVector3 vertices[3]{ v0, v1, v2 };
const DoubleVector3 sts[3]{ DoubleVector3( p0->m_texcoord ),
DoubleVector3( p1->m_texcoord ),
DoubleVector3( p2->m_texcoord ) };
Texdef_Construct_local2tex_from_ST( vertices, sts, m_local2tex );
m_tex2local = matrix4_affine_inverse( m_local2tex );
BP_from_ST( m_bp, vertices, sts, plane3_for_points( vertices ).normal() );
m_bp.removeScale( patch->getShader()->getTexture().width, patch->getShader()->getTexture().height );
}
}
bool valid() const {
return !( !std::isfinite( m_local2tex[0] ) //nan
|| !std::isfinite( m_tex2local[0] ) //nan
|| fabs( vector3_dot( m_plane.normal(), m_tex2local.z().vec3() ) ) < 1e-6 //projected along face
|| vector3_length_squared( m_tex2local.x().vec3() ) < .01 //srsly scaled down, limit at max 10 textures per world unit
|| vector3_length_squared( m_tex2local.y().vec3() ) < .01 );
}
};
void Scene_PatchGetTexdef_Selected( scene::Graph& graph, TextureProjection &projection ){
if ( Patch* patch = Scene_GetUltimateSelectedVisiblePatch() ){
PatchTexdefConstructor c( patch );
if( c.valid() )
projection.m_brushprimit_texdef = c.m_bp;
}
}
bool Scene_PatchGetShaderTexdef_Selected( scene::Graph& graph, CopiedString& name, TextureProjection &projection ){
Patch* patch = nullptr;
if ( !( patch = Scene_GetUltimateSelectedVisiblePatch() ) )
Scene_forEachSelectedPatch( [&patch]( PatchInstance& p ){ if( !patch ) patch = &p.getPatch(); } );
if( patch ){
name = patch->GetShader();
PatchTexdefConstructor c( patch );
if( c.valid() )
projection.m_brushprimit_texdef = c.m_bp;
return true;
}
return false;
}
void Patch_SetTexdef( const float* hShift, const float* vShift, const float* hScale, const float* vScale, const float* rotation ){
Scene_forEachVisibleSelectedPatch( [hShift, vShift, hScale, vScale, rotation]( Patch& patch ){
PatchTexdefConstructor c( &patch );
if( c.valid() ){
BPTexdef_Assign( c.m_bp, hShift, vShift, hScale, vScale, rotation );
Matrix4 local2tex;
c.m_bp.addScale( patch.getShader()->getTexture().width, patch.getShader()->getTexture().height );
BP_Construct_local2tex( c.m_bp, c.m_plane, local2tex );
matrix4_multiply_by_matrix4( local2tex, c.m_tex2local );
patch.undoSave();
for( auto& p : patch.getControlPoints() )
p.m_texcoord = matrix4_transformed_point( local2tex, Vector3( p.m_texcoord ) ).vec2();
patch.controlPointsChanged();
}
else{ // fallback //. fixme: this is not cool; may be more valid cases in PatchTexdefConstructor, as in find one good triangle in problematic case
if( hShift )
Scene_PatchTranslateTexture_Selected( GlobalSceneGraph(), *hShift > 0? 8 : -8, 0 );
if( vShift )
Scene_PatchTranslateTexture_Selected( GlobalSceneGraph(), 0, *vShift > 0? 8 : -8 );
if( hScale )
Scene_PatchScaleTexture_Selected( GlobalSceneGraph(), *hScale > 0? .5 : -.5, 0 );
if( vScale )
Scene_PatchScaleTexture_Selected( GlobalSceneGraph(), 0, *vScale > 0? .5 : -.5 );
if( rotation )
Scene_PatchRotateTexture_Selected( GlobalSceneGraph(), *rotation > 0? 15 : -15 );
}
Patch_textureChanged();
} );
}