Refactoring editing tools

This commit is contained in:
2026-03-12 14:03:58 +03:00
parent 51dda7e79d
commit c557950cca
13 changed files with 2543 additions and 772 deletions

View File

@@ -77,6 +77,7 @@ add_subdirectory(assets/blender/vehicles)
add_subdirectory(assets/blender/buildings/parts)
add_subdirectory(assets/blender/characters)
add_subdirectory(resources)
add_subdirectory(src/text_editor)
add_executable(Game Game.cpp ${WATER_SRC})
target_include_directories(Game PRIVATE src/gamedata)

View File

@@ -74,7 +74,7 @@ add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend
${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
"${FEMALE_OBJECTS}"
"female"
@@ -85,7 +85,7 @@ add_custom_command(
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py
${VRM_IMPORTED_BLENDS}
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
VERBATIM
@@ -423,6 +423,7 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND)
endfunction()
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-bottom.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend)
add_clothes_pipeline(
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend" # INPUT_BLEND
@@ -430,3 +431,9 @@ add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend" # FINAL_OUTPUT_BLEND
)
add_clothes_pipeline(
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-bottom_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend" # FINAL_OUTPUT_BLEND
)

Binary file not shown.

View File

@@ -35,7 +35,7 @@ def get_transformation_matrices(obj):
return m, m_inv, m_normal
def raycast_and_adjust_vertices(target_body, bvh_cloth):
def raycast_and_adjust_vertices(target_body, bvh_cloth, out_ray_length=0.15):
"""Raycast from body to cloth and adjust vertices that intersect"""
m_body, m_body_inv, m_body_normal = get_transformation_matrices(target_body)
@@ -49,7 +49,7 @@ def raycast_and_adjust_vertices(target_body, bvh_cloth):
n_world = (m_body_normal @ v.normal).normalized()
# Raycast forward (into cloth) and backward (from inside cloth)
hit_f, _, _, _ = bvh_cloth.ray_cast(v_world, n_world, 0.015)
hit_f, _, _, _ = bvh_cloth.ray_cast(v_world, n_world, out_ray_length)
hit_b, _, _, _ = bvh_cloth.ray_cast(v_world, -n_world, 0.005)
if hit_f or hit_b:
@@ -151,9 +151,19 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
print(f"Processing: {clothing_name_for_final} -> {target_name} (using copy)")
# NEW CODE: Store custom properties from clothing before they might be lost
clothing_props = {}
for prop_name in ["ref_shapes", "ref_ray_length"]:
if prop_name in clothing_obj:
clothing_props[prop_name] = clothing_obj[prop_name]
print(f" Stored property '{prop_name}' = {clothing_obj[prop_name]} from clothing")
# Step A: Raycast & adjust vertices
out_ray_length = 0.015
if "ref_ray_length" in clothing_obj:
out_ray_length = clothing_obj["ref_ray_length"]
bvh_cloth = setup_bvh_and_matrices(clothing_obj)
hit_values = raycast_and_adjust_vertices(new_target, bvh_cloth)
hit_values = raycast_and_adjust_vertices(new_target, bvh_cloth, out_ray_length)
# Step B: Remove hidden geometry
protect_and_remove_hidden_geometry(new_target, hit_values)
@@ -188,6 +198,12 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
bpy.context.view_layer.objects.active = new_target
bpy.ops.object.join()
# NEW CODE: Copy stored custom properties to combined object
# After joining, new_target is the active object and contains the combined mesh
for prop_name, prop_value in clothing_props.items():
new_target[prop_name] = prop_value
print(f" Copied property '{prop_name}' = {prop_value} to combined object")
# Rename the combined object using the appropriate clothing name
new_target.name = f"{target_name}_{clothing_name_for_final}"
whitelist.add(new_target)

View File

@@ -29,35 +29,63 @@ def parse_args():
print("Usage: blender -b --python transfer_shape_keys.py -- <source_file> <target_file> <output_file>")
sys.exit(1)
def load_source_data(source_path):
"""Load source file and extract shape key information"""
def get_source_object_name(target_obj_info):
"""Get source object name from target's custom property or fallback values"""
# Check for custom property "ref_shapes" in the stored info
if target_obj_info.get('ref_shapes'):
ref_name = target_obj_info['ref_shapes']
print(f" Using ref_shapes custom property: '{ref_name}'")
return ref_name
# Fallback to "Body_shapes" then "Body"
print(f" No ref_shapes property found, trying fallbacks")
return None
def load_source_data(source_path, target_obj_info):
"""Load source file and extract shape key information using object name from target info"""
print(f"\nLoading source file: {source_path}")
if not os.path.exists(source_path):
print(f"Error: Source file not found: {source_path}")
sys.exit(1)
current_file = bpy.data.filepath if bpy.data.filepath else None
# Get source object name based on target's custom property from stored info
source_obj_name = get_source_object_name(target_obj_info)
# Open source file
bpy.ops.wm.open_mainfile(filepath=source_path)
body_object = None
for obj in bpy.data.objects:
if obj.name == "Body_shapes" and obj.type == 'MESH':
body_object = obj
break
# If we have a specific name from ref_shapes, try that first
if source_obj_name:
for obj in bpy.data.objects:
if obj.name == source_obj_name and obj.type == 'MESH':
body_object = obj
print(f" Found source object: '{source_obj_name}' from ref_shapes")
break
# If not found or no ref_shapes, try fallbacks
if not body_object:
fallback_names = ["Body_shapes", "Body"]
for fallback_name in fallback_names:
print(f" Trying fallback: '{fallback_name}'")
for obj in bpy.data.objects:
if obj.name == fallback_name and obj.type == 'MESH':
body_object = obj
source_obj_name = fallback_name
print(f" Found source object: '{fallback_name}'")
break
if body_object:
break
if not body_object:
for obj in bpy.data.objects:
if obj.name == "Body" and obj.type == 'MESH':
body_object = obj
break
if not body_object:
print("Error: Could not find mesh object named 'Body' in source file")
print("Error: Could not find mesh object in source file")
print("Tried: ref_shapes property (if present), 'Body_shapes', 'Body'")
sys.exit(1)
if not body_object.data.shape_keys:
print("Error: Body object has no shape keys")
print(f"Error: Source object '{source_obj_name}' has no shape keys")
sys.exit(1)
shape_key_data = {
@@ -73,7 +101,7 @@ def load_source_data(source_path):
shape_key_data['polygons'].append([v for v in poly.vertices])
source_shape_keys = body_object.data.shape_keys.key_blocks
print(f"Found Body object with {len(source_shape_keys)} shape keys")
print(f"Found source object '{source_obj_name}' with {len(source_shape_keys)} shape keys")
for sk in source_shape_keys:
shape_key_data['names'].append(sk.name)
@@ -85,15 +113,13 @@ def load_source_data(source_path):
shape_key_data['vertex_positions'][sk.name] = vertex_positions
if current_file and os.path.exists(current_file):
bpy.ops.wm.open_mainfile(filepath=current_file)
elif current_file:
bpy.ops.wm.read_homefile()
# Store the source object name that was actually used
shape_key_data['source_obj_name'] = source_obj_name
return shape_key_data
def load_target_file(target_path):
"""Load the target .blend file and find objects with custom properties"""
"""Load the target .blend file and collect information about target objects"""
print(f"\nLoading target file: {target_path}")
if not os.path.exists(target_path):
@@ -103,23 +129,41 @@ def load_target_file(target_path):
temp_dir = os.path.join(os.path.dirname(target_path), "temp_blend_files")
os.makedirs(temp_dir, exist_ok=True)
temp_target = os.path.join(temp_dir, os.path.basename(target_path))
# Create a unique temp file path
temp_target = os.path.join(temp_dir, f"temp_processing_{os.path.basename(target_path)}")
shutil.copy2(target_path, temp_target)
bpy.ops.wm.open_mainfile(filepath=temp_target)
target_objects = []
# Store information about target objects without keeping references
target_objects_info = []
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.data:
if all(prop in obj for prop in ['age', 'sex', 'slot']):
target_objects.append(obj)
print(f"Found target object: {obj.name}")
print(f" - age: {obj['age']}")
print(f" - sex: {obj['sex']}")
print(f" - slot: {obj['slot']}")
print(f" - vertices: {len(obj.data.vertices)}")
obj_info = {
'name': obj.name,
'age': obj['age'],
'sex': obj['sex'],
'slot': obj['slot'],
'vertex_count': len(obj.data.vertices),
'ref_shapes': obj.get('ref_shapes', None) # Store ref_shapes if present
}
target_objects_info.append(obj_info)
print(f"Found target object: {obj_info['name']}")
print(f" - age: {obj_info['age']}")
print(f" - sex: {obj_info['sex']}")
print(f" - slot: {obj_info['slot']}")
print(f" - vertices: {obj_info['vertex_count']}")
if obj_info['ref_shapes']:
print(f" - ref_shapes: '{obj_info['ref_shapes']}'")
return target_objects, temp_target
return target_objects_info, temp_target
def get_target_object_by_name(obj_name):
"""Get a valid reference to a target object by name"""
if obj_name in bpy.data.objects:
return bpy.data.objects[obj_name]
return None
def delete_existing_shape_keys(target_obj):
"""Delete all existing shape keys from target object"""
@@ -921,6 +965,31 @@ def save_output_file(output_path, temp_target):
print(f"File saved successfully")
def process_target_object(target_obj_info, source_data, current_file_path):
"""Process a single target object using its stored information"""
print(f"\nProcessing: {target_obj_info['name']}")
print(f" {'=' * 40}")
# Get a fresh reference to the target object
target_obj = get_target_object_by_name(target_obj_info['name'])
if not target_obj:
print(f" Error: Could not find target object '{target_obj_info['name']}'")
return False
ensure_shape_keys_structure(source_data['names'], target_obj)
mapping = transfer_shape_keys(source_data, target_obj)
verify_transfer(source_data['names'], target_obj)
# Test quality of 'fat' shape key if it exists
for sk_name in source_data['names']:
if sk_name == 'fat':
test_shape_key_quality(target_obj, sk_name, mapping, source_data)
break
print(f" {'=' * 40}")
print(f" ✓ Completed")
return True
def main():
print("=" * 60)
print("Blender Shape Key Transfer Script - Boundary Velocity Limiting")
@@ -932,29 +1001,60 @@ def main():
print(f"Output: {output_file}")
try:
source_data = load_source_data(source_file)
target_objects, temp_target = load_target_file(target_file)
# First, load target file and collect information about target objects
target_objects_info, temp_target = load_target_file(target_file)
if target_objects:
for idx, target_obj in enumerate(target_objects, 1):
print(f"\n[{idx}/{len(target_objects)}] Processing: {target_obj.name}")
print(f" {'=' * 40}")
ensure_shape_keys_structure(source_data['names'], target_obj)
mapping = transfer_shape_keys(source_data, target_obj)
verify_transfer(source_data['names'], target_obj)
for sk_name in source_data['names']:
if sk_name == 'fat':
test_shape_key_quality(target_obj, sk_name, mapping, source_data)
break
print(f" {'=' * 40}")
print(f" ✓ Completed")
save_output_file(output_file, temp_target)
else:
if not target_objects_info:
print("\nNo target objects found with required properties")
sys.exit(1)
print(f"\nFound {len(target_objects_info)} target objects to process")
# Create a working file that we'll update after each object
working_file = temp_target
# Process each target object
for idx, target_obj_info in enumerate(target_objects_info, 1):
print(f"\n{'=' * 60}")
print(f"[{idx}/{len(target_objects_info)}] Processing: {target_obj_info['name']}")
print(f"{'=' * 60}")
# Load source data using the target object's information
source_data = load_source_data(source_file, target_obj_info)
# Load the current working file
bpy.ops.wm.open_mainfile(filepath=working_file)
# Process the target object
success = process_target_object(target_obj_info, source_data, source_file)
if not success:
print(f" Failed to process {target_obj_info['name']}")
continue
# Save progress to a new temp file
temp_progress_file = os.path.join(os.path.dirname(working_file), f"progress_{idx}_{os.path.basename(target_file)}")
bpy.ops.wm.save_as_mainfile(filepath=temp_progress_file)
# Update working file for next iteration
working_file = temp_progress_file
print(f"\nProgress saved to: {temp_progress_file}")
# Copy the final working file to the output location
print(f"\nCopying final result to: {output_file}")
shutil.copy2(working_file, output_file)
# Clean up temp files
try:
temp_dir = os.path.dirname(temp_target)
for file in os.listdir(temp_dir):
if file.startswith("temp_") or file.startswith("progress_"):
os.remove(os.path.join(temp_dir, file))
if os.path.exists(temp_dir) and not os.listdir(temp_dir):
os.rmdir(temp_dir)
except:
pass
print("\n" + "=" * 60)
print("Script completed successfully!")
@@ -968,3 +1068,4 @@ def main():
if __name__ == "__main__":
main()

View File

@@ -13,7 +13,7 @@ add_library(GameData STATIC GameData.cpp CharacterModule.cpp WaterModule.cpp Sun
VehicleManagerModule.cpp AppModule.cpp StaticGeometryModule.cpp SmartObject.cpp SlotsModule.cpp QuestModule.cpp
PlayerActionModule.cpp CharacterAIModule.cpp goap.cpp AnimationSystem.cpp)
target_link_libraries(GameData PUBLIC
items luamodule
items luamodule text_editor
flecs::flecs_static
nlohmann_json::nlohmann_json
OgreMain

View File

@@ -1227,6 +1227,7 @@ struct EditorGUIListener : public Ogre::RenderTargetListener {
if (changed)
StaticGeometryModule::saveTemplates();
}
Items::itemsEditor();
if (ImGui::SmallButton("Run script for all towns..."))
Items::runScriptsForAllTowns();
displayItems();
@@ -1399,8 +1400,8 @@ EditorGUIModule::EditorGUIModule(flecs::world &ecs)
.without<EditorGUIData>()
.each([](const RenderWindow &window, const App &app, GUI &gui) {
float vpScale = window.dpi / 96;
if (vpScale < 1.0f)
vpScale = 1.0f;
if (vpScale < 1.0f)
vpScale = 1.0f;
Ogre::OverlayManager::getSingleton().setPixelRatio(
vpScale);
std::cout << "Editor GUI configure\n";

View File

@@ -13,6 +13,6 @@ target_link_libraries(items PRIVATE
OgreMain
OgreBites
editor
physics
physics text_editor
Tracy::TracyClient
)

View File

@@ -208,7 +208,855 @@ void createItemsMenu()
ImGui::EndMenu();
}
}
static void _setCameraSelectedPos(flecs::entity e)
{
Ogre::Vector3 cursorPos;
Ogre::Quaternion cursorOrientation;
// setCursorSelectedPos(e, cursorPos, cursorOrientation);
StaticGeometryModule::getItemPositionAndRotation(e, cursorPos,
cursorOrientation);
ECS::get<EditorGizmo>().sceneNode->_setDerivedPosition(cursorPos);
ECS::get<EditorGizmo>().sceneNode->_setDerivedOrientation(
cursorOrientation);
Ogre::Vector3 cameraPos =
ECS::get<Camera>().mCameraPivot->_getDerivedPosition();
Ogre::Vector3 cameraOffset =
cursorOrientation * Ogre::Vector3::UNIT_Z * 30.0f;
cameraPos.x = cursorPos.x;
cameraPos.z = cursorPos.z;
cameraPos += cameraOffset;
cameraPos.y = TerrainModule::get_height(
ECS::get<Terrain>().mTerrainGroup, cameraPos) +
10.0f;
Ogre::TerrainGroup *tg = ECS::get<Terrain>().mTerrainGroup;
long x, y;
tg->convertWorldPositionToTerrainSlot(cameraPos, &x, &y);
if (tg->getTerrain(x, y) && tg->getTerrain(x, y)->isLoaded()) {
float height = tg->getHeightAtWorldPosition(cameraPos);
cameraPos.y = height + 10.0f;
}
ECS::get<Camera>().mCameraPivot->_setDerivedPosition(cameraPos);
ECS::get<Camera>().mCameraPivot->_setDerivedOrientation(
cursorOrientation);
cameraPos = ECS::get<Camera>().mCameraGoal->_getDerivedPosition();
ECS::get<Camera>().mCameraNode->_setDerivedPosition(cameraPos);
ECS::get<Camera>().mCameraNode->_setDerivedOrientation(
ECS::get<Camera>().mCameraGoal->_getDerivedOrientation());
// updateHeightmap();
}
void drawRectPreview(nlohmann::json::json_pointer ptr,
const nlohmann::json &item)
{
// Reserve a square area spanning across the table
ImVec2 canvas_p0 = ImGui::GetCursorScreenPos();
ImVec2 canvas_sz = ImVec2(ImGui::GetContentRegionAvail().x, 200);
ImVec2 canvas_p1 =
ImVec2(canvas_p0.x + canvas_sz.x, canvas_p0.y + canvas_sz.y);
Ogre::String rectName = ptr.back();
nlohmann::json::json_pointer tmpptr = ptr;
tmpptr.pop_back();
const nlohmann::json &colorRects = item[tmpptr];
ImDrawList *draw_list = ImGui::GetWindowDrawList();
// Draw background
draw_list->AddRectFilled(canvas_p0, canvas_p1,
IM_COL32(50, 50, 50, 255));
for (auto &[name, data] : colorRects.items()) {
std::cout << data.dump() << std::endl;
// Map 0..1 to Screen Pixels
ImVec2 p_min =
ImVec2(canvas_p0.x + (float)data["left"] * canvas_sz.x,
canvas_p0.y + (float)data["top"] * canvas_sz.y);
ImVec2 p_max = ImVec2(
canvas_p0.x + (float)data["right"] * canvas_sz.x,
canvas_p0.y + (float)data["bottom"] * canvas_sz.y);
// Handle Color (assuming rgba are 0..255 or 0..1)
ImVec4 col = ImVec4(data["color"]["r"], data["color"]["g"],
data["color"]["b"], data["color"]["a"]);
// Draw the rect
draw_list->AddRectFilled(p_min, p_max,
ImGui::ColorConvertFloat4ToU32(col));
// Outline if selected
if (rectName == name)
draw_list->AddRect(p_min, p_max,
IM_COL32(255, 255, 0, 255), 0.0f, 0,
2.0f);
}
ImGui::Dummy(canvas_sz); // Advance cursor
}
#if 1
bool DrawGridSelectorInTableCell(const char *id, ImVec2 &outValue,
float cellSize = 20.0f)
{
bool changed = false;
static ImVec2 hoveredCell(-1, -1);
// Push style to ensure consistent spacing
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0));
ImDrawList *drawList = ImGui::GetWindowDrawList();
ImVec2 cursorPos = ImGui::GetCursorScreenPos();
for (int y = 0; y < 10; y++) {
for (int x = 0; x < 10; x++) {
ImVec2 cellMin = ImVec2(cursorPos.x + x * cellSize,
cursorPos.y + y * cellSize);
ImVec2 cellMax = ImVec2(cellMin.x + cellSize,
cellMin.y + cellSize);
// Use a dummy for spacing and hit detection
ImGui::PushID(y * 10 + x);
ImGui::Dummy(ImVec2(cellSize, cellSize));
// Check for interaction
if (ImGui::IsItemHovered()) {
hoveredCell = ImVec2(x / 9.0f, y / 9.0f);
if (ImGui::IsMouseClicked(0)) {
outValue = hoveredCell;
changed = true;
}
}
ImGui::PopID();
// Draw cell
bool isSelected = (outValue.x == x / 9.0f &&
outValue.y == y / 9.0f);
bool isHovered = (hoveredCell.x == x / 9.0f &&
hoveredCell.y == y / 9.0f);
ImU32 color;
if (isSelected)
color = IM_COL32(100, 150, 255, 255);
else if (isHovered)
color = IM_COL32(80, 80, 120, 255);
else
color = IM_COL32(60, 60, 70, 255);
drawList->AddRectFilled(cellMin, cellMax, color);
drawList->AddRect(cellMin, cellMax,
IM_COL32(200, 200, 200, 255));
if (x < 9)
ImGui::SameLine();
}
}
// Move cursor past the grid
ImGui::Dummy(ImVec2(1, 1));
ImGui::PopStyleVar(2);
return changed;
}
enum EditType {
EditNone = 0,
EditString,
EditMultilineString,
EditColorRect,
EditFloat,
EditBool,
EditInt,
ViewJson,
EditItemPosition,
EditLuaScript,
};
struct PropertyEditor;
// Property editor definition
struct PropertyEditor {
int editType;
std::string name;
std::function<bool(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j)>
matches;
std::function<void(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j)>
render;
std::function<void(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j)>
apply;
};
struct DPropertyEditor : PropertyEditor {
private:
virtual bool _matches(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) = 0;
virtual void _render(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) = 0;
virtual void _apply(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j)
{
}
public:
DPropertyEditor(int type, const std::string &name)
{
editType = type;
this->name = name;
matches = [this](const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) -> bool {
return _matches(itemName, ptr, j);
};
render = [this](flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) { _render(entity, ptr, j); };
apply = [this](flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) { _apply(entity, ptr, j); };
}
PropertyEditor getPropertyEditor()
{
return { editType, name, matches, render, apply };
}
virtual ~DPropertyEditor()
{
}
};
struct StringPropertyEditor : DPropertyEditor {
bool _matches(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) override
{
return j[ptr].is_string();
}
void _render(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) override
{
static char buffer[260];
strncpy(buffer, j[ptr].get<Ogre::String>().c_str(),
sizeof(buffer));
ImGui::TableSetColumnIndex(0);
ImGui::Text("%s", ptr.back().c_str());
ImGui::TableSetColumnIndex(1);
ImGui::InputText("##value", buffer, sizeof(buffer));
if (ImGui::IsItemDeactivatedAfterEdit()) {
j[ptr] = buffer;
StaticGeometryModule::setItemProperties(entity,
j.dump());
}
}
StringPropertyEditor()
: DPropertyEditor(EditString, "String Editor")
{
}
~StringPropertyEditor() override
{
}
};
// EditLuaScript
struct LuaScriptPropertyEditor : DPropertyEditor {
bool _matches(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) override
{
return itemName == "cellScript" && j[ptr].is_string();
}
void _render(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) override
{
static char buffer[16384];
strncpy(buffer, j[ptr].get<Ogre::String>().c_str(),
sizeof(buffer));
ImGui::TableSetColumnIndex(0);
ImGui::Text("%s", ptr.back().c_str());
ImGui::TableSetColumnIndex(1);
ImGui::InputTextMultiline("##value", buffer, sizeof(buffer),
ImVec2(0, 0),
ImGuiInputTextFlags_AllowTabInput);
if (ImGui::IsItemDeactivatedAfterEdit()) {
j[ptr] = buffer;
StaticGeometryModule::setItemProperties(entity,
j.dump());
}
}
LuaScriptPropertyEditor()
: DPropertyEditor(EditLuaScript, "Lua Script Editor")
{
}
~LuaScriptPropertyEditor() override
{
}
};
struct MultilineStringPropertyEditor : DPropertyEditor {
bool _matches(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) override
{
return j[ptr].is_string() &&
(j[ptr].get<std::string>().find("\n") !=
std::string::npos ||
j[ptr].get<std::string>().length() >= 100);
}
void _render(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) override
{
static char buffer[4096];
strncpy(buffer, j[ptr].get<Ogre::String>().c_str(),
sizeof(buffer));
ImGui::TableSetColumnIndex(0);
ImGui::Text("%s", ptr.back().c_str());
ImGui::TableSetColumnIndex(1);
ImGui::InputTextMultiline("##value", buffer, sizeof(buffer));
if (ImGui::IsItemDeactivatedAfterEdit()) {
j[ptr] = buffer;
StaticGeometryModule::setItemProperties(entity,
j.dump());
}
}
MultilineStringPropertyEditor()
: DPropertyEditor(EditMultilineString,
"Multiline String Editor")
{
}
~MultilineStringPropertyEditor() override
{
}
};
// Float editor
struct FloatPropertyEditor : DPropertyEditor {
bool _matches(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) override
{
return j[ptr].is_number_float();
}
void _render(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) override
{
ImGui::TableSetColumnIndex(0);
ImGui::Text("%s", ptr.back().c_str());
ImGui::TableSetColumnIndex(1);
float val = j[ptr].get<float>();
if (ImGui::InputFloat("##value", &val, 0.001f, 0.01f)) {
j[ptr] = val;
StaticGeometryModule::setItemProperties(entity,
j.dump());
StaticGeometryModule::updateItemGeometry(entity);
}
}
FloatPropertyEditor()
: DPropertyEditor(EditFloat, "Float Editor")
{
}
~FloatPropertyEditor() override
{
}
};
// int editor
struct IntPropertyEditor : DPropertyEditor {
bool _matches(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) override
{
return j[ptr].is_number_integer();
}
void _render(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) override
{
ImGui::TableSetColumnIndex(0);
ImGui::Text("%s", ptr.back().c_str());
ImGui::TableSetColumnIndex(1);
int val = j[ptr].get<int>();
if (ImGui::InputInt("##value", &val, 1, 10)) {
j[ptr] = val;
StaticGeometryModule::setItemProperties(entity,
j.dump());
StaticGeometryModule::updateItemGeometry(entity);
}
}
IntPropertyEditor()
: DPropertyEditor(EditInt, "Int Editor")
{
}
~IntPropertyEditor() override
{
}
};
// bool editor
struct BoolPropertyEditor : DPropertyEditor {
bool _matches(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) override
{
return j[ptr].is_boolean();
}
void _render(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) override
{
ImGui::TableSetColumnIndex(0);
ImGui::Text("%s", ptr.back().c_str());
ImGui::TableSetColumnIndex(1);
bool val = j[ptr].get<bool>();
if (ImGui::Checkbox("##value", &val)) {
j[ptr] = val;
StaticGeometryModule::setItemProperties(entity,
j.dump());
StaticGeometryModule::updateItemGeometry(entity);
}
}
BoolPropertyEditor()
: DPropertyEditor(EditBool, "Bool Editor")
{
}
~BoolPropertyEditor() override
{
}
};
// color rect editor
struct ColorRectPropertyEditor : DPropertyEditor {
bool _matches(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) override
{
if (ptr.empty())
return false;
if (!ptr.parent_pointer().empty() &&
ptr.parent_pointer().back() == "colorRects") {
return true;
}
return false;
}
void _render(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) override
{
bool updateMaterial = false;
ImGui::TableSetColumnIndex(1);
drawRectPreview(ptr, j);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Bounds");
ImGui::TableSetColumnIndex(1);
nlohmann::json &d = j[ptr];
nlohmann::json &c = j[ptr]["color"];
float bounds[2] = { d["left"], d["top"] };
if (ImGui::InputFloat("Left", &bounds[0], 0.1f, 0.1f, "%.1f")) {
d["left"] = std::clamp(std::round(bounds[0] * 10.0f) /
10.0f,
0.0f, 1.0f);
d["right"] = std::clamp(
std::round((bounds[0] + 0.1f) * 10.0f) / 10.0f,
0.0f, 1.0f);
updateMaterial = true;
}
if (ImGui::InputFloat("Top", &bounds[1], 0.1f, 0.1f, "%.1f")) {
d["top"] = std::clamp(std::round(bounds[1] * 10.0f) /
10.0f,
0.0f, 1.0f);
d["bottom"] = std::clamp(
std::round((bounds[1] + 0.1f) * 10.0f) / 10.0f,
0.0f, 1.0f);
updateMaterial = true;
}
ImVec2 value;
value.x = bounds[0];
value.y = bounds[1];
if (DrawGridSelectorInTableCell("##selectRect", value)) {
bounds[0] = value.x;
bounds[1] = value.y;
d["left"] = std::clamp(std::round(bounds[0] * 10.0f) /
10.0f,
0.0f, 1.0f);
d["right"] = std::clamp(
std::round((bounds[0] + 0.1f) * 10.0f) / 10.0f,
0.0f, 1.0f);
d["top"] = std::clamp(std::round(bounds[1] * 10.0f) /
10.0f,
0.0f, 1.0f);
d["bottom"] = std::clamp(
std::round((bounds[1] + 0.1f) * 10.0f) / 10.0f,
0.0f, 1.0f);
updateMaterial = true;
}
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Text("Color");
ImGui::TableSetColumnIndex(1);
float col[4] = { c["r"], c["g"], c["b"], c["a"] };
if (ImGui::ColorEdit4("##color", col)) {
c["r"] = col[0];
c["g"] = col[1];
c["b"] = col[2];
c["a"] = col[3];
updateMaterial = true;
}
if (updateMaterial) {
StaticGeometryModule::setItemProperties(entity,
j.dump());
Geometry::createTownMaterial(entity, true);
}
}
ColorRectPropertyEditor()
: DPropertyEditor(EditColorRect, "Color Rect Editor")
{
}
~ColorRectPropertyEditor() override
{
}
};
template <typename T> struct registerPropertyEditor {
registerPropertyEditor(std::vector<PropertyEditor> &reg)
{
static T regdata;
reg.push_back(regdata);
}
};
static std::vector<PropertyEditor> propertyEditors;
// ViewJson editor (fallback)
struct FallbackPropertyEditor : DPropertyEditor {
bool _matches(const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) override
{
return true;
}
void _render(flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) override
{
ImGui::TableSetColumnIndex(1);
ImGui::TextWrapped("%s", j[ptr].dump(4).c_str());
}
FallbackPropertyEditor()
: DPropertyEditor(ViewJson, "JSON Viewer")
{
}
};
static registerPropertyEditor<StringPropertyEditor>
stringPropertyEditor(propertyEditors);
static registerPropertyEditor<LuaScriptPropertyEditor>
luaScriptPropertyEditor(propertyEditors);
static registerPropertyEditor<MultilineStringPropertyEditor>
multilineStringPropertyEditor(propertyEditors);
static registerPropertyEditor<FloatPropertyEditor>
floatPropertyEditor(propertyEditors);
static registerPropertyEditor<IntPropertyEditor>
intPropertyEditor(propertyEditors);
static registerPropertyEditor<BoolPropertyEditor>
boolPropertyEditor(propertyEditors);
static registerPropertyEditor<ColorRectPropertyEditor>
colorRectPropertyEditor(propertyEditors);
static registerPropertyEditor<FallbackPropertyEditor>
fallbackPropertyEditor(propertyEditors);
void itemsEditor()
{
struct EditorState {
flecs::entity edited;
nlohmann::json::json_pointer edited_ptr;
int edited_type;
EditorState()
: edited_type(EditNone)
{
}
void clear()
{
edited = flecs::entity();
edited_ptr = nlohmann::json::json_pointer();
edited_type = EditNone;
}
};
static EditorState state;
// Helper to find matching editor
auto findEditor = [](int editType, const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) -> PropertyEditor * {
for (auto &editor : propertyEditors) {
if (editor.editType == editType &&
editor.matches(itemName, ptr, j)) {
return &editor;
}
}
return nullptr;
};
// Leaf node detection - keeping your working logic
auto is_leaf = [](const std::string &itemName,
const nlohmann::json::json_pointer &ptr) -> bool {
// Direct key matches
if (itemName == "name" || itemName == "position" ||
itemName == "orientation" || itemName == "rotation" ||
itemName == "cells" || itemName == "furniture_cells" ||
itemName == "roofs" || itemName == "pierPath" ||
itemName == "cellScript") {
return true;
}
// Parent-based matches
if (!ptr.empty() && !ptr.parent_pointer().empty()) {
std::string parent = ptr.parent_pointer().back();
if (parent == "colorRects" || parent == "npcs" ||
parent == "centerBuildings") {
return true;
}
}
return false;
};
// Edit type determination
auto set_edit_type = [&findEditor](
const std::string &itemName,
const nlohmann::json::json_pointer &ptr,
const nlohmann::json &j) {
// Try to find by specific rules first
if (itemName == "name") {
state.edited_type = EditString;
} else if (itemName == "position" ||
itemName == "orientation" ||
itemName == "rotation" || itemName == "cells" ||
itemName == "furniture_cells" ||
itemName == "roofs" || itemName == "pierPath") {
state.edited_type = ViewJson;
} else if (itemName == "cellScript") {
state.edited_type = EditMultilineString;
} else if (!ptr.empty() && !ptr.parent_pointer().empty()) {
std::string parent = ptr.parent_pointer().back();
if (parent == "colorRects") {
state.edited_type = EditColorRect;
} else if (parent == "npcs" ||
parent == "centerBuildings") {
state.edited_type = ViewJson;
} else {
// Fall back to type-based detection
if (j[ptr].is_number_float())
state.edited_type = EditFloat;
else if (j[ptr].is_number_integer())
state.edited_type = EditInt;
else if (j[ptr].is_boolean())
state.edited_type = EditBool;
else if (j[ptr].is_string())
state.edited_type = EditString;
else
state.edited_type = ViewJson;
}
} else {
// Fall back to type-based detection
if (j[ptr].is_number_float())
state.edited_type = EditFloat;
else if (j[ptr].is_number_integer())
state.edited_type = EditInt;
else if (j[ptr].is_boolean())
state.edited_type = EditBool;
else if (j[ptr].is_string())
state.edited_type = EditString;
else
state.edited_type = ViewJson;
}
};
// Recursive hierarchy renderer
std::function<void(
const Ogre::String &, flecs::entity, ImGuiTreeNodeFlags,
const nlohmann::json::json_pointer &, nlohmann::json &)>
renderHierarchy;
renderHierarchy = [&renderHierarchy, &is_leaf, &set_edit_type](
const Ogre::String &label, flecs::entity item,
ImGuiTreeNodeFlags flags,
const nlohmann::json::json_pointer &ptr,
nlohmann::json &j) {
// Generate a unique ID for this node
ImGui::PushID(
static_cast<int>(reinterpret_cast<intptr_t>(&j[ptr])));
// Set flags
ImGuiTreeNodeFlags nodeFlags = flags;
if (state.edited == item && state.edited_ptr == ptr)
nodeFlags |= ImGuiTreeNodeFlags_Selected;
// Render the node
std::string displayLabel = label;
if (!ptr.empty() && j[ptr].is_string() &&
ptr.back() == "name") {
// Optionally show value for name fields
displayLabel += ": " + j[ptr].get<std::string>();
}
bool nodeOpen = ImGui::TreeNodeEx((void *)&j[ptr], nodeFlags,
"%s", displayLabel.c_str());
// Handle click
if (ImGui::IsItemClicked()) {
state.edited = item;
state.edited_ptr = ptr;
std::string itemName = ptr.empty() ? "" : ptr.back();
set_edit_type(itemName, ptr, j);
std::cout << ptr << std::endl;
}
// Render children
if (nodeOpen) {
std::string itemName = ptr.empty() ? "" : ptr.back();
bool leaf = is_leaf(itemName, ptr);
if (!leaf) {
if (j[ptr].is_array()) {
for (size_t i = 0; i < j[ptr].size();
++i) {
auto nptr = ptr / i;
renderHierarchy(
std::to_string(i), item,
flags, nptr, j);
}
} else if (j[ptr].is_object()) {
nlohmann::json &obj = j[ptr];
for (auto it = obj.begin();
it != obj.end(); ++it) {
auto nptr = ptr / it.key();
renderHierarchy(it.key(), item,
flags, nptr, j);
}
}
}
ImGui::TreePop();
}
ImGui::PopID();
};
// Property editor rendering using registry
auto renderPropertyEditor =
[&findEditor](flecs::entity entity,
const nlohmann::json::json_pointer &ptr,
int editType) {
Ogre::String jsonStr =
StaticGeometryModule::getItemProperties(entity);
nlohmann::json j = nlohmann::json::parse(jsonStr);
std::string itemName = ptr.empty() ? "" : ptr.back();
PropertyEditor *editor =
findEditor(editType, itemName, ptr, j);
if (editor) {
editor->render(entity, ptr, j);
} else {
// Fallback
ImGui::TableSetColumnIndex(1);
ImGui::TextWrapped("%s",
j[ptr].dump(4).c_str());
}
};
if (ImGui::SmallButton("Edit items..."))
ImGui::OpenPopup("Edit Items Popup");
if (ImGui::BeginPopupModal("Edit Items Popup", NULL,
ImGuiWindowFlags_MenuBar)) {
// Optional: Add a menu bar for "Add District/Lot" actions
if (ImGui::BeginMenuBar()) {
if (ImGui::BeginMenu("File")) { /* ... */
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
// --- LEFT SIDE: HIERARCHY ---
ImGui::BeginChild("HierarchyRegion", ImVec2(300, 400), true);
std::list<std::pair<flecs::entity, Ogre::String> > items;
StaticGeometryModule::getItemsProperties(&items);
ImGuiTreeNodeFlags baseFlags =
ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_SpanAvailWidth |
ImGuiTreeNodeFlags_DefaultOpen;
for (const auto &itemPair : items) {
flecs::entity entity = itemPair.first;
const Ogre::String &jsonStr = itemPair.second;
ImGui::PushID(static_cast<int>(entity.id()));
nlohmann::json j = nlohmann::json::parse(jsonStr);
// Create label
Ogre::String label =
Ogre::StringConverter::toString(entity.id());
label += ":" + j["type"].get<Ogre::String>();
if (j.find("name") != j.end() &&
!j["name"].get<Ogre::String>().empty())
label = j["name"].get<Ogre::String>();
// Render hierarchy for this item
renderHierarchy(label, entity, baseFlags,
nlohmann::json::json_pointer(), j);
ImGui::PopID();
}
ImGui::EndChild();
ImGui::SameLine();
// --- RIGHT SIDE: PROPERTIES ---
ImGui::BeginChild("PropertiesRegion", ImVec2(0, 400), true);
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding,
ImVec2(10.0f, 4.0f));
if (ImGui::BeginTable("PropTable", 2,
ImGuiTableFlags_SizingStretchProp |
ImGuiTableFlags_Resizable)) {
ImGui::TableSetupColumn(
"Property", ImGuiTableColumnFlags_WidthFixed,
0.0f);
ImGui::TableSetupColumn(
"Value", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
ImGui::TableNextRow();
if (state.edited.is_valid() &&
!state.edited_ptr.empty()) {
renderPropertyEditor(state.edited,
state.edited_ptr,
state.edited_type);
} else if (state.edited.is_valid()) {
// Show item summary when no specific property is selected
ImGui::TableSetColumnIndex(0);
ImGui::Text("Item");
ImGui::TableSetColumnIndex(1);
Ogre::String jsonStr =
StaticGeometryModule::getItemProperties(
state.edited);
nlohmann::json j =
nlohmann::json::parse(jsonStr);
ImGui::TextWrapped("%s", j.dump(4).c_str());
}
ImGui::EndTable();
}
ImGui::PopStyleVar();
// DrawPropertyInspector(); // Uses the Table logic from before
ImGui::EndChild();
if (ImGui::Button("Close")) {
ImGui::CloseCurrentPopup();
state.clear();
}
ImGui::EndPopup();
}
}
#endif
}
namespace Geometry
{

View File

@@ -12,6 +12,7 @@ void showItemPopup(const std::pair<flecs::entity, Ogre::String> &item);
void showItemButtons(const std::pair<flecs::entity, Ogre::String> &item);
void createItemsMenu();
void runScriptsForAllTowns();
void itemsEditor();
}
namespace Geometry
{

File diff suppressed because it is too large Load Diff