From f503689e1ebe441cc1f4a7da0dc0cfaed35d978e Mon Sep 17 00:00:00 2001 From: AGulev Date: Fri, 23 Jan 2026 23:51:23 +0100 Subject: [PATCH 1/5] add outline and properties --- .../bundle_resources/web/static/codepad.css | 126 ++++++- .../bundle_resources/web/static/codepad.js | 290 +++++++++++++++- codepad/template.html | 35 +- game.project | 4 +- scene_dump/ext.manifest | 1 + scene_dump/src/scene_dump.cpp | 322 ++++++++++++++++++ 6 files changed, 756 insertions(+), 22 deletions(-) create mode 100644 scene_dump/ext.manifest create mode 100644 scene_dump/src/scene_dump.cpp diff --git a/codepad/bundle_resources/web/static/codepad.css b/codepad/bundle_resources/web/static/codepad.css index 69a5cfe..894980b 100644 --- a/codepad/bundle_resources/web/static/codepad.css +++ b/codepad/bundle_resources/web/static/codepad.css @@ -157,10 +157,18 @@ svg, .svg-image { margin-right: 6px; } -#pane-editor, #pane-console { +#pane-editor, #pane-console, #pane-canvas, #row-top, #row-bottom, #inspector-pane { position: relative; } +#inspector-wrap { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; +} + #editor-wrap { width: 100%; position: absolute; @@ -219,6 +227,122 @@ svg, .svg-image { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; } +#hierarchy-pane, #properties-pane { + position: relative; + height: 100%; +} + +#hierarchy-tree, #properties-list { + position: absolute; + top: 38px; + bottom: 8px; + left: 8px; + right: 8px; + overflow: auto; + font-size: 12px; +} + +#hierarchy-pane .tabs-wrap, #properties-pane .tabs-wrap { + background-color: #2a2d32; + border-bottom: 1px solid #1f2125; +} + +#hierarchy-pane .tabs-wrap label, #properties-pane .tabs-wrap label { + opacity: 1; + border-bottom: none; + color: #cfd3d7; + font-weight: 600; +} + +.tree-item { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 6px; + border-radius: 2px; + cursor: pointer; +} + +.tree-item:hover { + background-color: #34373c; +} + +.tree-item.is-selected { + background-color: #3d4046; + color: #ffffff; +} + +.tree-caret { + width: 10px; + height: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + color: #9aa0a6; + font-size: 10px; + flex: 0 0 10px; +} + +.tree-caret::before { + content: "\25B8"; + transform: rotate(0deg); + transition: transform 0.15s ease; +} + +.tree-node.is-expanded > .tree-item .tree-caret::before { + transform: rotate(90deg); +} + +.tree-node.is-collapsed > .tree-children { + display: none; +} + +.tree-label { + font-weight: 600; +} + +.tree-meta { + margin-left: auto; + opacity: 0.6; + font-size: 11px; +} + +.tree-children { + margin-left: 14px; + border-left: 1px solid #32353a; + padding-left: 6px; +} + +.properties-header { + font-weight: 600; + margin-bottom: 8px; + color: #eff2f6; +} + +.prop-row { + display: grid; + grid-template-columns: 120px 1fr; + gap: 8px; + padding: 6px 0px; + border-bottom: 1px solid #35383d; +} + +.prop-key { + color: #9aa0a6; + text-transform: capitalize; +} + +.prop-value { + background-color: #25282d; + border: 1px solid #3a3d43; + border-radius: 3px; + color: #d0d4d9; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; + white-space: pre-wrap; + word-break: break-word; + padding: 4px 6px; +} + .gutter { } diff --git a/codepad/bundle_resources/web/static/codepad.js b/codepad/bundle_resources/web/static/codepad.js index e22fb5f..750b8f2 100644 --- a/codepad/bundle_resources/web/static/codepad.js +++ b/codepad/bundle_resources/web/static/codepad.js @@ -15,6 +15,9 @@ var scenes = []; var project_info = {}; var engine_info = {}; +var scene_hierarchy = null; +var scene_node_index = {}; +var scene_selected_path = null; var default_script = `function init(self) @@ -87,14 +90,39 @@ function codepad_load_editor(callback) { //editor.session.setMode("ace/mode/lua"); // Setup panel splitters - Split(['#pane-editors', '#pane-canvas'], { - direction: 'vertical', - onDrag: function () { fix_canvas_size(); } - }); + if (document.getElementById("row-top") && document.getElementById("row-bottom")) { + Split(['#row-top', '#row-bottom'], { + direction: 'vertical', + sizes: [55, 45], + minSize: [160, 160], + onDrag: function () { fix_canvas_size(); } + }); + } - Split(['#pane-console', '#pane-editor'], { - sizes: [30, 70] - }); + if (document.getElementById("pane-console") && document.getElementById("pane-editor")) { + Split(['#pane-console', '#pane-editor'], { + direction: 'horizontal', + sizes: [30, 70], + minSize: [180, 320] + }); + } + + if (document.getElementById("inspector-pane") && document.getElementById("pane-canvas")) { + Split(['#inspector-pane', '#pane-canvas'], { + direction: 'horizontal', + sizes: [30, 70], + minSize: [180, 320], + onDrag: function () { fix_canvas_size(); } + }); + } + + if (document.getElementById("hierarchy-pane") && document.getElementById("properties-pane")) { + Split(['#hierarchy-pane', '#properties-pane'], { + direction: 'vertical', + sizes: [60, 40], + minSize: [80, 80] + }); + } if (callback) { callback(); @@ -234,6 +262,7 @@ function codepad_ready(scenes_json, project_json, engine_json) { codepad_trigger_url_check(); codepad_change_scene(); + setTimeout(codepad_dump_hierarchy, 0); } /** @@ -271,6 +300,243 @@ function codepad_restart() { codepad_should_restart = true; } +function codepad_dump_hierarchy() { + if (typeof Module === "undefined" || !Module.ccall) { + console.warn("Scene dump unavailable: Module.ccall is missing."); + return; + } + try { + var ptr = Module.ccall("CodepadSceneDump_DumpJson", "number", [], []); + if (!ptr) { + console.warn("Scene dump returned no data."); + return; + } + var json = Module.UTF8ToString(ptr); + var data = JSON.parse(json); + scene_hierarchy = data; + codepad_index_hierarchy(data); + codepad_render_hierarchy(data); + console.log("Scene hierarchy:", data); + return data; + } catch (err) { + console.error("Scene dump failed:", err); + } +} + +function codepad_escape_html(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); +} + +function codepad_index_hierarchy(node) { + scene_node_index = {}; + if (!node) { + return; + } + (function walk(current) { + if (current && current.path) { + scene_node_index[current.path] = current; + } + if (current && current.children) { + for (var i = 0; i < current.children.length; i++) { + walk(current.children[i]); + } + } + })(node); +} + +function codepad_build_tree_node(node) { + var wrapper = document.createElement("div"); + var hasChildren = node.children && node.children.length; + wrapper.className = "tree-node" + (hasChildren ? " is-expanded" : ""); + + var item = document.createElement("div"); + item.className = "tree-item"; + item.dataset.path = node.path || ""; + + var caret = document.createElement("span"); + caret.className = "tree-caret"; + if (!hasChildren) { + caret.style.visibility = "hidden"; + } + item.appendChild(caret); + + var label = document.createElement("span"); + label.className = "tree-label"; + label.textContent = node.name || "(unnamed)"; + item.appendChild(label); + + if (node.type) { + var meta = document.createElement("span"); + meta.className = "tree-meta"; + meta.textContent = node.type; + item.appendChild(meta); + } + + wrapper.appendChild(item); + + if (hasChildren) { + var children = document.createElement("div"); + children.className = "tree-children"; + for (var i = 0; i < node.children.length; i++) { + children.appendChild(codepad_build_tree_node(node.children[i])); + } + wrapper.appendChild(children); + } + + return wrapper; +} + +function codepad_render_hierarchy(tree) { + var container = document.getElementById("hierarchy-tree"); + if (!container) { + return; + } + container.innerHTML = ""; + if (!tree) { + return; + } + container.appendChild(codepad_build_tree_node(tree)); + codepad_bind_hierarchy_events(); + if (tree.path) { + codepad_select_node(tree.path); + } +} + +function codepad_bind_hierarchy_events() { + var container = document.getElementById("hierarchy-tree"); + if (!container || container._codepadBound) { + return; + } + container._codepadBound = true; + container.addEventListener("click", function (event) { + var caret = event.target.closest(".tree-caret"); + if (caret) { + var nodeElem = caret.closest(".tree-node"); + if (nodeElem && nodeElem.classList.contains("is-expanded")) { + nodeElem.classList.remove("is-expanded"); + nodeElem.classList.add("is-collapsed"); + } else if (nodeElem && nodeElem.classList.contains("is-collapsed")) { + nodeElem.classList.remove("is-collapsed"); + nodeElem.classList.add("is-expanded"); + } + event.stopPropagation(); + return; + } + var item = event.target.closest(".tree-item"); + if (!item) { + return; + } + var path = item.dataset.path; + if (!path) { + return; + } + codepad_select_node(path); + }); +} + +function codepad_select_node(path) { + var container = document.getElementById("hierarchy-tree"); + if (!container) { + return; + } + var previous = container.querySelector(".tree-item.is-selected"); + if (previous) { + previous.classList.remove("is-selected"); + } + var next = container.querySelector('.tree-item[data-path="' + path + '"]'); + if (next) { + next.classList.add("is-selected"); + } + scene_selected_path = path; + codepad_render_properties(scene_node_index[path]); +} + +function codepad_format_prop_value(value) { + if (value === null || value === undefined) { + return "null"; + } + if (Array.isArray(value)) { + return "[" + value.map(codepad_format_prop_value).join(", ") + "]"; + } + if (typeof value === "object") { + try { + return JSON.stringify(value); + } catch (err) { + return String(value); + } + } + return String(value); +} + +function codepad_render_properties(node) { + var container = document.getElementById("properties-list"); + if (!container) { + return; + } + container.innerHTML = ""; + if (!node) { + return; + } + + var header = document.createElement("div"); + header.className = "properties-header"; + header.textContent = node.path || node.name || "Node"; + container.appendChild(header); + + var props = {}; + props.name = node.name || ""; + if (node.path) { + props.path = node.path; + } + if (node.props) { + for (var key in node.props) { + if (node.props.hasOwnProperty(key)) { + props[key] = node.props[key]; + } + } + } + + var priority = ["id", "name", "path", "url", "position", "rotation", "scale", "size", "pivot", "anchorPoint", "visible", "enabled", "layer"]; + var keys = Object.keys(props).sort(); + keys.sort(function (a, b) { + var ai = priority.indexOf(a); + var bi = priority.indexOf(b); + if (ai === -1 && bi === -1) { + return a.localeCompare(b); + } + if (ai === -1) { + return 1; + } + if (bi === -1) { + return -1; + } + return ai - bi; + }); + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var row = document.createElement("div"); + row.className = "prop-row"; + + var keySpan = document.createElement("span"); + keySpan.className = "prop-key"; + keySpan.textContent = key; + + var valueSpan = document.createElement("span"); + valueSpan.className = "prop-value"; + valueSpan.innerHTML = codepad_escape_html(codepad_format_prop_value(props[key])); + + row.appendChild(keySpan); + row.appendChild(valueSpan); + container.appendChild(row); + } +} + function codepad_get_code(i) { if (codepad_sessions[i - 1]) { if (EditSession !== undefined) { @@ -471,8 +737,14 @@ function codepad_show_play_embed(callback) { }; splash.innerHTML = "
Run code
"; document.body.classList += "embedded"; - var pane_editors = document.getElementById("pane-editors"); - pane_editors.remove(); + var row_top = document.getElementById("row-top"); + if (row_top) { + row_top.remove(); + } + var inspector = document.getElementById("inspector-pane"); + if (inspector) { + inspector.remove(); + } } function codepad_start(callback) { diff --git a/codepad/template.html b/codepad/template.html index 92fe3f7..df17828 100644 --- a/codepad/template.html +++ b/codepad/template.html @@ -32,12 +32,12 @@
-
-
-
-
+
+
+
+
- +
@@ -49,12 +49,27 @@
-
-
- +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+ +
- -
diff --git a/game.project b/game.project index 09c2690..340e5e2 100644 --- a/game.project +++ b/game.project @@ -1,9 +1,8 @@ [project] title = DefoldCodePad -version = 0.1 +version = 0.2 bundle_resources = /codepad/bundle_resources/ bundle_exclude_resources = -custom_resources = main/scripts [bootstrap] main_collection = /main/main.collectionc @@ -28,4 +27,5 @@ shared_state = 1 [html5] htmlfile = /codepad/template.html +scale_mode = stretch diff --git a/scene_dump/ext.manifest b/scene_dump/ext.manifest new file mode 100644 index 0000000..218a528 --- /dev/null +++ b/scene_dump/ext.manifest @@ -0,0 +1 @@ +name: "codepad_scene_dump" diff --git a/scene_dump/src/scene_dump.cpp b/scene_dump/src/scene_dump.cpp new file mode 100644 index 0000000..12e3f70 --- /dev/null +++ b/scene_dump/src/scene_dump.cpp @@ -0,0 +1,322 @@ +#define EXTENSION_NAME codepad_scene_dump +#define LIB_NAME "codepad_scene_dump" +#define MODULE_NAME "codepad_scene_dump" +#ifndef DLIB_LOG_DOMAIN +#define DLIB_LOG_DOMAIN LIB_NAME +#endif +#include + +#include +#include + +#if defined(DM_PLATFORM_HTML5) +#include +#endif + +#include +#include +#include + +namespace +{ + struct SceneDumpContext + { + dmGameObject::HRegister m_Register; + bool m_Initialized; + }; + + SceneDumpContext g_Context = { 0, false }; + std::string g_Buffer; + + static void AppendJsonString(std::string& out, const char* value) + { + out.push_back('"'); + if (value) + { + const unsigned char* p = (const unsigned char*)value; + while (*p) + { + unsigned char c = *p++; + switch (c) + { + case '"': out.append("\\\""); break; + case '\\': out.append("\\\\"); break; + case '\b': out.append("\\b"); break; + case '\f': out.append("\\f"); break; + case '\n': out.append("\\n"); break; + case '\r': out.append("\\r"); break; + case '\t': out.append("\\t"); break; + default: + if (c < 0x20) + { + char buf[7]; + snprintf(buf, sizeof(buf), "\\u%04x", (unsigned int)c); + out.append(buf); + } + else + { + out.push_back((char)c); + } + break; + } + } + } + out.push_back('"'); + } + + static void AppendJsonNumber(std::string& out, double value) + { + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%.6g", value); + out.append(buffer); + } + + static void AppendJsonBool(std::string& out, bool value) + { + out.append(value ? "true" : "false"); + } + + static void AppendField(std::string& out, const char* key, const char* value, bool* first) + { + if (!*first) + { + out.push_back(','); + } + *first = false; + AppendJsonString(out, key); + out.push_back(':'); + if (value) + { + AppendJsonString(out, value); + } + else + { + out.append("null"); + } + } + + static void AppendJsonVector(std::string& out, const float* value, int count) + { + out.push_back('['); + for (int i = 0; i < count; ++i) + { + if (i > 0) + { + out.push_back(','); + } + AppendJsonNumber(out, value[i]); + } + out.push_back(']'); + } + + static void AppendPropertyValue(std::string& out, dmGameObject::SceneNodeProperty* property) + { + switch (property->m_Type) + { + case dmGameObject::SCENE_NODE_PROPERTY_TYPE_HASH: + { + const char* value = dmHashReverseSafe64(property->m_Value.m_Hash); + if (value) + { + AppendJsonString(out, value); + } + else + { + out.append("null"); + } + break; + } + case dmGameObject::SCENE_NODE_PROPERTY_TYPE_NUMBER: + AppendJsonNumber(out, property->m_Value.m_Number); + break; + case dmGameObject::SCENE_NODE_PROPERTY_TYPE_BOOLEAN: + AppendJsonBool(out, property->m_Value.m_Bool); + break; + case dmGameObject::SCENE_NODE_PROPERTY_TYPE_URL: + AppendJsonString(out, property->m_Value.m_URL); + break; + case dmGameObject::SCENE_NODE_PROPERTY_TYPE_TEXT: + AppendJsonString(out, property->m_Value.m_Text); + break; + case dmGameObject::SCENE_NODE_PROPERTY_TYPE_VECTOR3: + AppendJsonVector(out, property->m_Value.m_V4, 3); + break; + case dmGameObject::SCENE_NODE_PROPERTY_TYPE_VECTOR4: + case dmGameObject::SCENE_NODE_PROPERTY_TYPE_QUAT: + AppendJsonVector(out, property->m_Value.m_V4, 4); + break; + default: + out.append("null"); + break; + } + } + + static void AppendProperty(std::string& out, dmGameObject::SceneNodeProperty* property, bool* first) + { + const char* key = dmHashReverseSafe64(property->m_NameHash); + if (!key || key[0] == '\0') + { + return; + } + if (strcmp(key, "id") == 0 || strcmp(key, "type") == 0) + { + return; + } + if (!*first) + { + out.push_back(','); + } + *first = false; + AppendJsonString(out, key); + out.push_back(':'); + AppendPropertyValue(out, property); + } + + static void GetNodeInfo(dmGameObject::SceneNode* node, dmhash_t& name, dmhash_t& type) + { + static dmhash_t hash_id = dmHashString64("id"); + static dmhash_t hash_type = dmHashString64("type"); + + dmGameObject::SceneNodePropertyIterator pit = TraverseIterateProperties(node); + while (dmGameObject::TraverseIteratePropertiesNext(&pit)) + { + if (pit.m_Property.m_NameHash == hash_id) + { + name = pit.m_Property.m_Value.m_Hash; + } + else if (pit.m_Property.m_NameHash == hash_type) + { + type = pit.m_Property.m_Value.m_Hash; + } + } + } + + static void DumpNode(std::string& out, dmGameObject::SceneNode* node, const std::string& parent_path, int index) + { + dmhash_t name_hash = 0; + dmhash_t type_hash = 0; + GetNodeInfo(node, name_hash, type_hash); + + const char* name_str = name_hash ? dmHashReverseSafe64(name_hash) : 0; + if (!name_str || name_str[0] == '\0') + { + name_str = "node"; + } + + const char* type_str = type_hash ? dmHashReverseSafe64(type_hash) : 0; + + std::string path; + if (!parent_path.empty()) + { + path.reserve(parent_path.size() + 32); + path.append(parent_path); + path.push_back('/'); + } + else + { + path.reserve(32); + path.push_back('/'); + } + path.append(name_str); + if (index >= 0) + { + char index_buf[16]; + snprintf(index_buf, sizeof(index_buf), "[%d]", index); + path.append(index_buf); + } + + out.push_back('{'); + bool first = true; + AppendField(out, "name", name_str, &first); + AppendField(out, "type", type_str, &first); + AppendField(out, "path", path.c_str(), &first); + + out.append(",\"props\":{"); + bool props_first = true; + AppendField(out, "id", name_str, &props_first); + if (type_str) + { + AppendField(out, "type", type_str, &props_first); + } + dmGameObject::SceneNodePropertyIterator pit = TraverseIterateProperties(node); + while (dmGameObject::TraverseIteratePropertiesNext(&pit)) + { + AppendProperty(out, &pit.m_Property, &props_first); + } + out.push_back('}'); + + out.append(",\"children\":["); + dmGameObject::SceneNodeIterator it = dmGameObject::TraverseIterateChildren(node); + int child_index = 0; + bool first_child = true; + while (dmGameObject::TraverseIterateNext(&it)) + { + if (!first_child) + { + out.push_back(','); + } + first_child = false; + DumpNode(out, &it.m_Node, path, child_index); + ++child_index; + } + out.push_back(']'); + out.push_back('}'); + } + + static const char* BuildSceneJson() + { + g_Buffer.clear(); + if (!g_Context.m_Initialized) + { + g_Buffer.assign("null"); + return g_Buffer.c_str(); + } + + dmGameObject::SceneNode root; + if (!dmGameObject::TraverseGetRoot(g_Context.m_Register, &root)) + { + g_Buffer.assign("null"); + return g_Buffer.c_str(); + } + + g_Buffer.reserve(4096); + DumpNode(g_Buffer, &root, std::string(), 0); + return g_Buffer.c_str(); + } +} + +#if defined(DM_PLATFORM_HTML5) +extern "C" EMSCRIPTEN_KEEPALIVE const char* CodepadSceneDump_DumpJson() +{ + return BuildSceneJson(); +} +#else +extern "C" const char* CodepadSceneDump_DumpJson() +{ + return 0; +} +#endif + +static dmExtension::Result AppInitializeSceneDump(dmExtension::AppParams* params) +{ + g_Context.m_Register = dmEngine::GetGameObjectRegister(params); + g_Context.m_Initialized = true; + return dmExtension::RESULT_OK; +} + +static dmExtension::Result InitializeSceneDump(dmExtension::Params* params) +{ + return dmExtension::RESULT_OK; +} + +static dmExtension::Result AppFinalizeSceneDump(dmExtension::AppParams* params) +{ + return dmExtension::RESULT_OK; +} + +static dmExtension::Result FinalizeSceneDump(dmExtension::Params* params) +{ + return dmExtension::RESULT_OK; +} + +DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, AppInitializeSceneDump, AppFinalizeSceneDump, InitializeSceneDump, 0, 0, FinalizeSceneDump) From c3ed46adccbae8b2a9e93823290224afb0b70588 Mon Sep 17 00:00:00 2001 From: AGulev Date: Sat, 24 Jan 2026 00:26:58 +0100 Subject: [PATCH 2/5] scene view fixes --- .../bundle_resources/web/static/codepad.css | 1 - .../bundle_resources/web/static/codepad.js | 233 ++++++++++++++---- scene_dump/src/scene_dump.cpp | 176 +++++++++++-- 3 files changed, 339 insertions(+), 71 deletions(-) diff --git a/codepad/bundle_resources/web/static/codepad.css b/codepad/bundle_resources/web/static/codepad.css index 894980b..cc92ae4 100644 --- a/codepad/bundle_resources/web/static/codepad.css +++ b/codepad/bundle_resources/web/static/codepad.css @@ -324,7 +324,6 @@ svg, .svg-image { grid-template-columns: 120px 1fr; gap: 8px; padding: 6px 0px; - border-bottom: 1px solid #35383d; } .prop-key { diff --git a/codepad/bundle_resources/web/static/codepad.js b/codepad/bundle_resources/web/static/codepad.js index 750b8f2..76b3b1d 100644 --- a/codepad/bundle_resources/web/static/codepad.js +++ b/codepad/bundle_resources/web/static/codepad.js @@ -18,6 +18,11 @@ var engine_info = {}; var scene_hierarchy = null; var scene_node_index = {}; var scene_selected_path = null; +var scene_structure_signature = null; +var scene_dump_running = false; +var scene_dump_frame = 0; +var scene_dump_missing_warned = false; +var scene_dump_filter = null; var default_script = `function init(self) @@ -119,7 +124,7 @@ function codepad_load_editor(callback) { if (document.getElementById("hierarchy-pane") && document.getElementById("properties-pane")) { Split(['#hierarchy-pane', '#properties-pane'], { direction: 'vertical', - sizes: [60, 40], + sizes: [50, 50], minSize: [80, 80] }); } @@ -222,6 +227,9 @@ function codepad_change_scene() { break; } } + scene_structure_signature = null; + scene_selected_path = null; + codepad_set_dump_filter(); } @@ -262,7 +270,10 @@ function codepad_ready(scenes_json, project_json, engine_json) { codepad_trigger_url_check(); codepad_change_scene(); - setTimeout(codepad_dump_hierarchy, 0); + setTimeout(function () { + codepad_dump_hierarchy(true); + codepad_start_dump_loop(); + }, 0); } /** @@ -300,29 +311,104 @@ function codepad_restart() { codepad_should_restart = true; } -function codepad_dump_hierarchy() { +function codepad_dump_hierarchy(silent) { if (typeof Module === "undefined" || !Module.ccall) { - console.warn("Scene dump unavailable: Module.ccall is missing."); + if (!scene_dump_missing_warned && !silent) { + console.warn("Scene dump unavailable: Module.ccall is missing."); + scene_dump_missing_warned = true; + } return; } try { + codepad_set_dump_filter(); var ptr = Module.ccall("CodepadSceneDump_DumpJson", "number", [], []); if (!ptr) { - console.warn("Scene dump returned no data."); + if (!silent) { + console.warn("Scene dump returned no data."); + } return; } var json = Module.UTF8ToString(ptr); var data = JSON.parse(json); + if (Array.isArray(data)) { + data = { _synthetic: true, children: data, props: { id: codepad_get_scene() || "scene" } }; + } scene_hierarchy = data; codepad_index_hierarchy(data); - codepad_render_hierarchy(data); - console.log("Scene hierarchy:", data); + var signature = codepad_build_structure_signature(data); + var structure_changed = signature !== scene_structure_signature; + scene_structure_signature = signature; + if (structure_changed || !document.getElementById("hierarchy-tree") || !document.getElementById("hierarchy-tree").hasChildNodes()) { + codepad_render_hierarchy(data); + } else { + codepad_render_properties(scene_node_index[scene_selected_path]); + } + if (!silent) { + console.log("Scene hierarchy:", data); + } return data; } catch (err) { console.error("Scene dump failed:", err); } } +function codepad_set_dump_filter() { + if (typeof Module === "undefined" || !Module.ccall) { + return; + } + var scene_id = codepad_get_scene(); + if (!scene_id || scene_id === scene_dump_filter) { + return; + } + Module.ccall("CodepadSceneDump_SetFilter", null, ["string"], [scene_id]); + scene_dump_filter = scene_id; +} + +function codepad_start_dump_loop() { + if (scene_dump_running) { + return; + } + scene_dump_running = true; + function tick() { + if (!scene_dump_running) { + return; + } + scene_dump_frame += 1; + if (scene_dump_frame % 2 === 0) { + codepad_dump_hierarchy(true); + } + requestAnimationFrame(tick); + } + requestAnimationFrame(tick); +} + +function codepad_build_structure_signature(node) { + var parts = []; + (function walk(current) { + if (!current) { + return; + } + if (current._synthetic) { + if (current.children) { + for (var i = 0; i < current.children.length; i++) { + walk(current.children[i]); + } + } + return; + } + parts.push(current._key || ""); + parts.push((current.props && current.props.id) || current.id || current.name || ""); + parts.push(current.type || ""); + var count = current.children ? current.children.length : 0; + parts.push(String(count)); + if (current.children) { + for (var i = 0; i < current.children.length; i++) { + walk(current.children[i]); + } + } + })(node); + return parts.join("|"); +} function codepad_escape_html(value) { return String(value) .replace(/&/g, "&") @@ -337,16 +423,28 @@ function codepad_index_hierarchy(node) { if (!node) { return; } - (function walk(current) { - if (current && current.path) { - scene_node_index[current.path] = current; + (function walk(current, parentKey, index) { + if (!current) { + return; + } + if (current._synthetic) { + if (current.children) { + for (var i = 0; i < current.children.length; i++) { + walk(current.children[i], parentKey || "", i); + } + } + return; } - if (current && current.children) { + var id = (current.props && current.props.id) || current.id || current.name || "node"; + var key = (parentKey ? parentKey + "/" : "") + id; + current._key = key; + scene_node_index[key] = current; + if (current.children) { for (var i = 0; i < current.children.length; i++) { - walk(current.children[i]); + walk(current.children[i], key, i); } } - })(node); + })(node, "", 0); } function codepad_build_tree_node(node) { @@ -356,7 +454,7 @@ function codepad_build_tree_node(node) { var item = document.createElement("div"); item.className = "tree-item"; - item.dataset.path = node.path || ""; + item.dataset.key = node._key || ""; var caret = document.createElement("span"); caret.className = "tree-caret"; @@ -367,7 +465,7 @@ function codepad_build_tree_node(node) { var label = document.createElement("span"); label.className = "tree-label"; - label.textContent = node.name || "(unnamed)"; + label.textContent = (node.props && node.props.id) || node.id || node.name || "(unnamed)"; item.appendChild(label); if (node.type) { @@ -400,10 +498,20 @@ function codepad_render_hierarchy(tree) { if (!tree) { return; } - container.appendChild(codepad_build_tree_node(tree)); + if (tree._synthetic && tree.children) { + for (var i = 0; i < tree.children.length; i++) { + container.appendChild(codepad_build_tree_node(tree.children[i])); + } + } else { + container.appendChild(codepad_build_tree_node(tree)); + } codepad_bind_hierarchy_events(); - if (tree.path) { - codepad_select_node(tree.path); + if (scene_selected_path && scene_node_index[scene_selected_path]) { + codepad_select_node(scene_selected_path); + } else if (tree._synthetic && tree.children && tree.children.length && tree.children[0]._key) { + codepad_select_node(tree.children[0]._key); + } else if (tree._key) { + codepad_select_node(tree._key); } } @@ -431,15 +539,15 @@ function codepad_bind_hierarchy_events() { if (!item) { return; } - var path = item.dataset.path; - if (!path) { + var key = item.dataset.key; + if (!key) { return; } - codepad_select_node(path); + codepad_select_node(key); }); } -function codepad_select_node(path) { +function codepad_select_node(key) { var container = document.getElementById("hierarchy-tree"); if (!container) { return; @@ -448,12 +556,12 @@ function codepad_select_node(path) { if (previous) { previous.classList.remove("is-selected"); } - var next = container.querySelector('.tree-item[data-path="' + path + '"]'); + var next = container.querySelector('.tree-item[data-key="' + key + '"]'); if (next) { next.classList.add("is-selected"); } - scene_selected_path = path; - codepad_render_properties(scene_node_index[path]); + scene_selected_path = key; + codepad_render_properties(scene_node_index[key]); } function codepad_format_prop_value(value) { @@ -478,21 +586,17 @@ function codepad_render_properties(node) { if (!container) { return; } - container.innerHTML = ""; if (!node) { + container.innerHTML = ""; + container._codepadNodeKey = null; + container._codepadKeySig = null; + container._codepadValueEls = null; + container._codepadRowEls = null; + container._codepadHeaderEl = null; return; } - var header = document.createElement("div"); - header.className = "properties-header"; - header.textContent = node.path || node.name || "Node"; - container.appendChild(header); - var props = {}; - props.name = node.name || ""; - if (node.path) { - props.path = node.path; - } if (node.props) { for (var key in node.props) { if (node.props.hasOwnProperty(key)) { @@ -500,6 +604,9 @@ function codepad_render_properties(node) { } } } + if (!props.id) { + props.id = (node.props && node.props.id) || node.id || node.name || node._key || "node"; + } var priority = ["id", "name", "path", "url", "position", "rotation", "scale", "size", "pivot", "anchorPoint", "visible", "enabled", "layer"]; var keys = Object.keys(props).sort(); @@ -518,22 +625,56 @@ function codepad_render_properties(node) { return ai - bi; }); + var node_key = node._key || props.id || "node"; + container._codepadNodeKey = node_key; + + if (!container._codepadHeaderEl) { + var header = document.createElement("div"); + header.className = "properties-header"; + container.appendChild(header); + container._codepadHeaderEl = header; + } + container._codepadHeaderEl.textContent = props.id || "Node"; + + if (!container._codepadRowEls) { + container._codepadRowEls = {}; + } + if (!container._codepadValueEls) { + container._codepadValueEls = {}; + } + for (var i = 0; i < keys.length; i++) { var key = keys[i]; - var row = document.createElement("div"); - row.className = "prop-row"; + var row = container._codepadRowEls[key]; + var valueSpan = container._codepadValueEls[key]; + if (!row || !valueSpan) { + row = document.createElement("div"); + row.className = "prop-row"; + + var keySpan = document.createElement("span"); + keySpan.className = "prop-key"; + keySpan.textContent = key; - var keySpan = document.createElement("span"); - keySpan.className = "prop-key"; - keySpan.textContent = key; + valueSpan = document.createElement("span"); + valueSpan.className = "prop-value"; - var valueSpan = document.createElement("span"); - valueSpan.className = "prop-value"; - valueSpan.innerHTML = codepad_escape_html(codepad_format_prop_value(props[key])); + row.appendChild(keySpan); + row.appendChild(valueSpan); + container.appendChild(row); - row.appendChild(keySpan); - row.appendChild(valueSpan); - container.appendChild(row); + container._codepadRowEls[key] = row; + container._codepadValueEls[key] = valueSpan; + } + row.style.display = "grid"; + valueSpan.textContent = codepad_format_prop_value(props[key]); + } + + for (var existing in container._codepadRowEls) { + if (container._codepadRowEls.hasOwnProperty(existing)) { + if (keys.indexOf(existing) === -1) { + container._codepadRowEls[existing].style.display = "none"; + } + } } } diff --git a/scene_dump/src/scene_dump.cpp b/scene_dump/src/scene_dump.cpp index 12e3f70..50504c3 100644 --- a/scene_dump/src/scene_dump.cpp +++ b/scene_dump/src/scene_dump.cpp @@ -27,6 +27,7 @@ namespace SceneDumpContext g_Context = { 0, false }; std::string g_Buffer; + std::string g_FilterId; static void AppendJsonString(std::string& out, const char* value) { @@ -158,7 +159,7 @@ namespace { return; } - if (strcmp(key, "id") == 0 || strcmp(key, "type") == 0) + if (strcmp(key, "id") == 0 || strcmp(key, "type") == 0 || strcmp(key, "resource") == 0 || strcmp(key, "script_id") == 0) { return; } @@ -191,45 +192,126 @@ namespace } } - static void DumpNode(std::string& out, dmGameObject::SceneNode* node, const std::string& parent_path, int index) + static const char* NormalizeType(const char* type_str, std::string& out) + { + if (!type_str) + { + return 0; + } + size_t len = strlen(type_str); + if (len > 0 && type_str[len - 1] == 'c') + { + out.assign(type_str, len - 1); + return out.c_str(); + } + return type_str; + } + + static bool IsCollectionProxyType(const char* type_str) + { + if (!type_str) + { + return false; + } + return strncmp(type_str, "collectionproxy", 15) == 0; + } + + static bool MatchesFilter(const char* name_str, const char* filter) + { + if (!filter || filter[0] == '\0') + { + return true; + } + if (!name_str || name_str[0] == '\0') + { + return false; + } + if (strcmp(name_str, filter) == 0) + { + return true; + } + if (filter[0] == '#' && strcmp(name_str, filter + 1) == 0) + { + return true; + } + if (name_str[0] == '#' && strcmp(name_str + 1, filter) == 0) + { + return true; + } + return false; + } + + static bool FindCollectionProxyById(dmGameObject::SceneNode* node, const char* filter, dmGameObject::SceneNode* out) { dmhash_t name_hash = 0; dmhash_t type_hash = 0; GetNodeInfo(node, name_hash, type_hash); const char* name_str = name_hash ? dmHashReverseSafe64(name_hash) : 0; - if (!name_str || name_str[0] == '\0') + const char* type_str = type_hash ? dmHashReverseSafe64(type_hash) : 0; + + if (IsCollectionProxyType(type_str) && MatchesFilter(name_str, filter)) { - name_str = "node"; + *out = *node; + return true; } - const char* type_str = type_hash ? dmHashReverseSafe64(type_hash) : 0; + dmGameObject::SceneNodeIterator it = dmGameObject::TraverseIterateChildren(node); + while (dmGameObject::TraverseIterateNext(&it)) + { + dmGameObject::SceneNode child = it.m_Node; + if (FindCollectionProxyById(&child, filter, out)) + { + return true; + } + } + return false; + } + + static bool FindFirstCollectionProxy(dmGameObject::SceneNode* node, dmGameObject::SceneNode* out) + { + dmhash_t name_hash = 0; + dmhash_t type_hash = 0; + GetNodeInfo(node, name_hash, type_hash); - std::string path; - if (!parent_path.empty()) + const char* type_str = type_hash ? dmHashReverseSafe64(type_hash) : 0; + if (IsCollectionProxyType(type_str)) { - path.reserve(parent_path.size() + 32); - path.append(parent_path); - path.push_back('/'); + *out = *node; + return true; } - else + + dmGameObject::SceneNodeIterator it = dmGameObject::TraverseIterateChildren(node); + while (dmGameObject::TraverseIterateNext(&it)) { - path.reserve(32); - path.push_back('/'); + dmGameObject::SceneNode child = it.m_Node; + if (FindFirstCollectionProxy(&child, out)) + { + return true; + } } - path.append(name_str); - if (index >= 0) + return false; + } + + static void DumpNode(std::string& out, dmGameObject::SceneNode* node) + { + dmhash_t name_hash = 0; + dmhash_t type_hash = 0; + GetNodeInfo(node, name_hash, type_hash); + + const char* name_str = name_hash ? dmHashReverseSafe64(name_hash) : 0; + if (!name_str || name_str[0] == '\0') { - char index_buf[16]; - snprintf(index_buf, sizeof(index_buf), "[%d]", index); - path.append(index_buf); + name_str = "node"; } + const char* raw_type_str = type_hash ? dmHashReverseSafe64(type_hash) : 0; + std::string type_clean; + const char* type_str = NormalizeType(raw_type_str, type_clean); + out.push_back('{'); bool first = true; - AppendField(out, "name", name_str, &first); AppendField(out, "type", type_str, &first); - AppendField(out, "path", path.c_str(), &first); out.append(",\"props\":{"); bool props_first = true; @@ -247,7 +329,6 @@ namespace out.append(",\"children\":["); dmGameObject::SceneNodeIterator it = dmGameObject::TraverseIterateChildren(node); - int child_index = 0; bool first_child = true; while (dmGameObject::TraverseIterateNext(&it)) { @@ -256,8 +337,7 @@ namespace out.push_back(','); } first_child = false; - DumpNode(out, &it.m_Node, path, child_index); - ++child_index; + DumpNode(out, &it.m_Node); } out.push_back(']'); out.push_back('}'); @@ -280,7 +360,47 @@ namespace } g_Buffer.reserve(4096); - DumpNode(g_Buffer, &root, std::string(), 0); + dmGameObject::SceneNode target = root; + bool found_target = false; + if (!g_FilterId.empty()) + { + found_target = FindCollectionProxyById(&root, g_FilterId.c_str(), &target); + if (!found_target) + { + found_target = FindFirstCollectionProxy(&root, &target); + } + } + if (found_target) + { + dmhash_t type_hash = 0; + dmhash_t name_hash = 0; + GetNodeInfo(&target, name_hash, type_hash); + const char* type_str = type_hash ? dmHashReverseSafe64(type_hash) : 0; + if (IsCollectionProxyType(type_str)) + { + g_Buffer.push_back('['); + dmGameObject::SceneNodeIterator it = dmGameObject::TraverseIterateChildren(&target); + bool first_child = true; + while (dmGameObject::TraverseIterateNext(&it)) + { + if (!first_child) + { + g_Buffer.push_back(','); + } + first_child = false; + DumpNode(g_Buffer, &it.m_Node); + } + g_Buffer.push_back(']'); + } + else + { + DumpNode(g_Buffer, &target); + } + } + else + { + DumpNode(g_Buffer, &root); + } return g_Buffer.c_str(); } } @@ -290,11 +410,19 @@ extern "C" EMSCRIPTEN_KEEPALIVE const char* CodepadSceneDump_DumpJson() { return BuildSceneJson(); } +extern "C" EMSCRIPTEN_KEEPALIVE void CodepadSceneDump_SetFilter(const char* filter) +{ + g_FilterId = filter ? filter : ""; +} #else extern "C" const char* CodepadSceneDump_DumpJson() { return 0; } +extern "C" void CodepadSceneDump_SetFilter(const char* filter) +{ + (void)filter; +} #endif static dmExtension::Result AppInitializeSceneDump(dmExtension::AppParams* params) From c7aa3c05216025dddcfa23319cfb4ca139c957ee Mon Sep 17 00:00:00 2001 From: AGulev Date: Sat, 24 Jan 2026 08:18:33 +0100 Subject: [PATCH 3/5] fix view --- .../bundle_resources/web/static/codepad.css | 5 + .../bundle_resources/web/static/codepad.js | 21 +- codepad/rendering/custom.render_script | 294 +++++++++++++++--- codepad/template.html | 14 + game.project | 5 + main/main.collection | 11 + 6 files changed, 306 insertions(+), 44 deletions(-) diff --git a/codepad/bundle_resources/web/static/codepad.css b/codepad/bundle_resources/web/static/codepad.css index cc92ae4..0c21241 100644 --- a/codepad/bundle_resources/web/static/codepad.css +++ b/codepad/bundle_resources/web/static/codepad.css @@ -358,6 +358,11 @@ svg, .svg-image { float: left; } +.split.split-vertical, .gutter.gutter-vertical { + width: 100%; + float: none; +} + .canvas-app-container { background: #000; position: relative; diff --git a/codepad/bundle_resources/web/static/codepad.js b/codepad/bundle_resources/web/static/codepad.js index 76b3b1d..66fe039 100644 --- a/codepad/bundle_resources/web/static/codepad.js +++ b/codepad/bundle_resources/web/static/codepad.js @@ -854,12 +854,23 @@ function codepad_is_embedded() { function fix_canvas_size(event) { var canvas = document.getElementById('canvas'); + if (!canvas) { + return; + } + var container = document.getElementById("app-container") || canvas.parentElement; + var rect = container ? container.getBoundingClientRect() : canvas.getBoundingClientRect(); + var width = rect.width; + var height = rect.height; if (codepad_is_embedded()) { - canvas.width = document.body.offsetWidth; - canvas.height = document.body.offsetHeight; - } else { - canvas.width = canvas.offsetWidth; - canvas.height = canvas.offsetHeight; + width = document.body.offsetWidth || width; + height = document.body.offsetHeight || height; + } + if (width > 0 && height > 0) { + var dpr = window.devicePixelRatio || 1; + canvas.style.width = Math.round(width) + "px"; + canvas.style.height = Math.round(height) + "px"; + canvas.width = Math.max(1, Math.round(width * dpr)); + canvas.height = Math.max(1, Math.round(height * dpr)); } } diff --git a/codepad/rendering/custom.render_script b/codepad/rendering/custom.render_script index f69ce7a..6f5e278 100644 --- a/codepad/rendering/custom.render_script +++ b/codepad/rendering/custom.render_script @@ -1,57 +1,273 @@ +-- Copyright 2020-2026 The Defold Foundation +-- Copyright 2014-2020 King +-- Copyright 2009-2014 Ragnar Svensson, Christian Murray +-- Licensed under the Defold License version 1.0 (the "License"); you may not use +-- this file except in compliance with the License. +-- +-- You may obtain a copy of the License, together with FAQs at +-- https://www.defold.com/license +-- +-- Unless required by applicable law or agreed to in writing, software distributed +-- under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +-- CONDITIONS OF ANY KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations under the License. + +-- +-- message constants +-- +local MSG_CLEAR_COLOR = hash("clear_color") +local MSG_WINDOW_RESIZED = hash("window_resized") +local MSG_SET_VIEW_PROJ = hash("set_view_projection") +local MSG_SET_CAMERA_PROJ = hash("use_camera_projection") +local MSG_USE_STRETCH_PROJ = hash("use_stretch_projection") +local MSG_USE_FIXED_PROJ = hash("use_fixed_projection") +local MSG_USE_FIXED_FIT_PROJ = hash("use_fixed_fit_projection") + +local DEFAULT_NEAR = -1 +local DEFAULT_FAR = 1 +local DEFAULT_ZOOM = 1 + +-- +-- projection that centers content with maintained aspect ratio and optional zoom +-- +local function get_fixed_projection(camera, state) + camera.zoom = camera.zoom or DEFAULT_ZOOM + local projected_width = state.window_width / camera.zoom + local projected_height = state.window_height / camera.zoom + local left = -(projected_width - state.width) / 2 + local bottom = -(projected_height - state.height) / 2 + local right = left + projected_width + local top = bottom + projected_height + return vmath.matrix4_orthographic(left, right, bottom, top, camera.near, camera.far) +end +-- +-- projection that centers and fits content with maintained aspect ratio +-- +local function get_fixed_fit_projection(camera, state) + camera.zoom = math.min(state.window_width / state.width, state.window_height / state.height) + return get_fixed_projection(camera, state) +end +-- +-- projection that stretches content +-- +local function get_stretch_projection(camera, state) + return vmath.matrix4_orthographic(0, state.width, 0, state.height, camera.near, camera.far) +end +-- +-- projection for gui +-- +local function get_gui_projection(camera, state) + return vmath.matrix4_orthographic(0, state.window_width, 0, state.window_height, camera.near, camera.far) +end + +local function update_clear_color(state, color) + if color then + state.clear_buffers[graphics.BUFFER_TYPE_COLOR0_BIT] = color + end +end + +local function update_camera(camera, state) + if camera.projection_fn then + camera.proj = camera.projection_fn(camera, state) + camera.options.frustum = camera.proj * camera.view + end +end + +local function update_state(state) + state.window_width = render.get_window_width() + state.window_height = render.get_window_height() + state.valid = state.window_width > 0 and state.window_height > 0 + if not state.valid then + return false + end + -- Make sure state updated only once when resize window + if state.window_width == state.prev_window_width and state.window_height == state.prev_window_height then + return true + end + state.prev_window_width = state.window_width + state.prev_window_height = state.window_height + state.width = render.get_width() + state.height = render.get_height() + for _, camera in pairs(state.cameras) do + update_camera(camera, state) + end + return true +end + +local function init_camera(camera, projection_fn, near, far, zoom) + camera.view = vmath.matrix4() + camera.near = near == nil and DEFAULT_NEAR or near + camera.far = far == nil and DEFAULT_FAR or far + camera.zoom = zoom == nil and DEFAULT_ZOOM or zoom + camera.projection_fn = projection_fn +end + +local function create_predicates(...) + local arg = {...} + local predicates = {} + for _, predicate_name in pairs(arg) do + predicates[predicate_name] = render.predicate({predicate_name}) + end + return predicates +end + +local function create_camera(state, name, is_main_camera) + local camera = {} + camera.options = {} + state.cameras[name] = camera + if is_main_camera then + state.main_camera = camera + end + return camera +end + +local function create_state() + local state = {} + local color = vmath.vector4(0, 0, 0, 0) + color.x = sys.get_config_number("render.clear_color_red", 0) + color.y = sys.get_config_number("render.clear_color_green", 0) + color.z = sys.get_config_number("render.clear_color_blue", 0) + color.w = sys.get_config_number("render.clear_color_alpha", 0) + state.clear_buffers = { + [graphics.BUFFER_TYPE_COLOR0_BIT] = color, + [graphics.BUFFER_TYPE_DEPTH_BIT] = 1, + [graphics.BUFFER_TYPE_STENCIL_BIT] = 0 + } + state.cameras = {} + return state +end + +local function set_camera_world(state) + local camera_components = camera.get_cameras() + + -- This will set the last enabled camera from the stack of camera components + if #camera_components > 0 then + for i = #camera_components, 1, -1 do + if camera.get_enabled(camera_components[i]) then + local camera_component = state.cameras.camera_component + camera_component.camera = camera_components[i] + render.set_camera(camera_component.camera, { use_frustum = true }) + -- The frustum will be overridden by the render.set_camera call, + -- so we don't need to return anything here other than an empty table. + return camera_component.options + end + end + end + + -- If no active camera was found, we use the default main "camera world" camera + local camera_world = state.cameras.camera_world + render.set_view(camera_world.view) + render.set_projection(camera_world.proj) + return camera_world.options +end + +local function reset_camera_world(state) + -- unbind the camera if a camera component is used + if state.cameras.camera_component.camera then + state.cameras.camera_component.camera = nil + render.set_camera() + end +end + function init(self) - self.tile_pred = render.predicate({"tile"}) - self.gui_pred = render.predicate({"gui"}) - self.text_pred = render.predicate({"text"}) - self.particle_pred = render.predicate({"particle"}) + self.predicates = create_predicates("tile", "gui", "particle", "model", "debug_text") - self.clear_color = vmath.vector4(44/255, 46/255, 51/255, 1) + -- default is stretch projection. copy from builtins and change for different projection + -- or send a message to the render script to change projection: + -- msg.post("@render:", "use_stretch_projection", { near = -1, far = 1 }) + -- msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = 2 }) + -- msg.post("@render:", "use_fixed_fit_projection", { near = -1, far = 1 }) - self.view = vmath.matrix4() + local state = create_state() + self.state = state + + local camera_world = create_camera(state, "camera_world", true) + init_camera(camera_world, get_stretch_projection) + local camera_gui = create_camera(state, "camera_gui") + init_camera(camera_gui, get_gui_projection) + -- Create a special camera that wraps camera components (if they exist) + -- It will take precedence over any other camera, and not change from messages + local camera_component = create_camera(state, "camera_component") + update_state(state) end function update(self) + local state = self.state + if not state.valid then + if not update_state(state) then + return + end + end + + local predicates = self.predicates + -- clear screen buffers + -- + -- turn on depth_mask before `render.clear()` to clear it as well render.set_depth_mask(true) render.set_stencil_mask(0xff) - render.clear({[render.BUFFER_COLOR_BIT] = self.clear_color, [render.BUFFER_DEPTH_BIT] = 1, [render.BUFFER_STENCIL_BIT] = 0}) - - render.set_viewport(0, 0, render.get_window_width(), render.get_window_height()) - - -- draw game objects - local hw = render.get_window_width() / 2 - local hh = render.get_window_height() / 2 - local projection = vmath.matrix4_orthographic(-hw, hw, -hh, hh, -1, 1) - local frustum = projection * self.view - render.set_view(self.view) - render.set_projection(projection) - + render.clear(state.clear_buffers) + + -- setup camera view and projection + -- + local draw_options_world = set_camera_world(state) + render.set_viewport(0, 0, state.window_width, state.window_height) + + -- set states used for all the world predicates + render.set_blend_func(graphics.BLEND_FACTOR_SRC_ALPHA, graphics.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA) + render.enable_state(graphics.STATE_DEPTH_TEST) + + -- render `model` predicate for default 3D material + -- + render.enable_state(graphics.STATE_CULL_FACE) + render.draw(predicates.model, draw_options_world) render.set_depth_mask(false) - render.disable_state(render.STATE_DEPTH_TEST) - render.disable_state(render.STATE_STENCIL_TEST) - render.disable_state(render.STATE_CULL_FACE) - render.enable_state(render.STATE_BLEND) - render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA) + render.disable_state(graphics.STATE_CULL_FACE) + -- render the other components: sprites, tilemaps, particles etc + -- + render.enable_state(graphics.STATE_BLEND) render.draw_debug3d() - render.draw(self.tile_pred, { frustum = frustum }) - render.draw(self.particle_pred, { frustum = frustum }) + render.draw(predicates.tile, draw_options_world) + render.draw(predicates.particle, draw_options_world) + render.disable_state(graphics.STATE_DEPTH_TEST) - -- draw gui - local view_gui = vmath.matrix4() - local proj_gui = vmath.matrix4_orthographic(0, render.get_window_width(), 0, render.get_window_height(), -1, 1) - local frustum_gui = proj_gui * view_gui - render.set_view(view_gui) - render.set_projection(proj_gui) + reset_camera_world(state) - render.enable_state(render.STATE_STENCIL_TEST) - render.draw(self.gui_pred, {frustum = frustum_gui}) - render.draw(self.text_pred, {frustum = frustum_gui}) - render.disable_state(render.STATE_STENCIL_TEST) + -- render GUI + -- + local camera_gui = state.cameras.camera_gui + render.set_view(camera_gui.view) + render.set_projection(camera_gui.proj) + + render.enable_state(graphics.STATE_STENCIL_TEST) + render.draw(predicates.gui, camera_gui.options) + render.draw(predicates.debug_text, camera_gui.options) + render.disable_state(graphics.STATE_STENCIL_TEST) + render.disable_state(graphics.STATE_BLEND) end function on_message(self, message_id, message) - if message_id == hash("clear_color") then - self.clear_color = message.color - elseif message_id == hash("set_view_projection") then - self.view = message.view + local state = self.state + local camera = state.main_camera + + if message_id == MSG_CLEAR_COLOR then + update_clear_color(state, message.color) + elseif message_id == MSG_WINDOW_RESIZED then + update_state(state) + elseif message_id == MSG_SET_VIEW_PROJ then + camera.view = message.view + self.camera_projection = message.projection or vmath.matrix4() + update_camera(camera, state) + elseif message_id == MSG_SET_CAMERA_PROJ then + camera.projection_fn = function() return self.camera_projection end + elseif message_id == MSG_USE_STRETCH_PROJ then + init_camera(camera, get_stretch_projection, message.near, message.far) + update_camera(camera, state) + elseif message_id == MSG_USE_FIXED_PROJ then + init_camera(camera, get_fixed_projection, message.near, message.far, message.zoom) + update_camera(camera, state) + elseif message_id == MSG_USE_FIXED_FIT_PROJ then + init_camera(camera, get_fixed_fit_projection, message.near, message.far) + update_camera(camera, state) end end diff --git a/codepad/template.html b/codepad/template.html index df17828..51e850a 100644 --- a/codepad/template.html +++ b/codepad/template.html @@ -84,8 +84,22 @@ }, engine_arguments: [{{#DEFOLD_ENGINE_ARGUMENTS}}"{{.}}",{{/DEFOLD_ENGINE_ARGUMENTS}}], custom_heap_size: {{DEFOLD_HEAP_SIZE}}, + full_screen_container: "#app-container", + resize_window_callback: function() { + if (typeof fix_canvas_size === "function") { + fix_canvas_size(); + } + }, disable_context_menu: true } + if (typeof CUSTOM_PARAMETERS !== "undefined") { + CUSTOM_PARAMETERS.full_screen_container = "#app-container"; + CUSTOM_PARAMETERS.resize_window_callback = function() { + if (typeof fix_canvas_size === "function") { + fix_canvas_size(); + } + }; + } Module['onRuntimeInitialized'] = function() { Module.runApp("canvas", extra_params); diff --git a/game.project b/game.project index 340e5e2..e91ccad 100644 --- a/game.project +++ b/game.project @@ -29,3 +29,8 @@ shared_state = 1 htmlfile = /codepad/template.html scale_mode = stretch +[render] +clear_color_green = 0.18 +clear_color_red = 0.17 +clear_color_blue = 0.2 + diff --git a/main/main.collection b/main/main.collection index 1fd357e..72c8caa 100644 --- a/main/main.collection +++ b/main/main.collection @@ -36,5 +36,16 @@ embedded_instances { " data: \"collection: \\\"/main/codepads/label/label.collection\\\"\\n" "\"\n" "}\n" + "embedded_components {\n" + " id: \"camera\"\n" + " type: \"camera\"\n" + " data: \"aspect_ratio: 1.0\\n" + "fov: 0.7854\\n" + "near_z: -1000.0\\n" + "far_z: 1000.0\\n" + "orthographic_projection: 1\\n" + "orthographic_mode: ORTHO_MODE_AUTO_FIT\\n" + "\"\n" + "}\n" "" } From 451ba636921655ec310384784a4bea2fdb1a1e88 Mon Sep 17 00:00:00 2001 From: AGulev Date: Sat, 24 Jan 2026 08:29:34 +0100 Subject: [PATCH 4/5] fix examples --- game.project | 1 + main/codepads/gui_nodes/gui_nodes.lua | 2 +- main/scripts/factory/factory.script | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/game.project b/game.project index e91ccad..b5a0bc4 100644 --- a/game.project +++ b/game.project @@ -3,6 +3,7 @@ title = DefoldCodePad version = 0.2 bundle_resources = /codepad/bundle_resources/ bundle_exclude_resources = +custom_resources = /main/scripts [bootstrap] main_collection = /main/main.collectionc diff --git a/main/codepads/gui_nodes/gui_nodes.lua b/main/codepads/gui_nodes/gui_nodes.lua index d82cc4c..5590094 100644 --- a/main/codepads/gui_nodes/gui_nodes.lua +++ b/main/codepads/gui_nodes/gui_nodes.lua @@ -51,7 +51,7 @@ end return { name = "Gui Nodes", url = "#cp_gui_nodes", - grid = false, + grid = true, scripts = { { url = "cp_gui_nodes:/go#gui", diff --git a/main/scripts/factory/factory.script b/main/scripts/factory/factory.script index ebc2845..2b6ae3a 100644 --- a/main/scripts/factory/factory.script +++ b/main/scripts/factory/factory.script @@ -16,7 +16,8 @@ end function on_input(self, action_id, action) if action_id == hash("mouse_button_left") and action.released then - local id = factory.create("#factory", vmath.vector3(action.x, action.y, 0)) + local world_pos = camera.screen_xy_to_world(action.screen_x, action.screen_y) + local id = factory.create("#factory", world_pos) print(id) end end From 8babae36febe1bc52de62df685aa4ba780e7e6ae Mon Sep 17 00:00:00 2001 From: AGulev Date: Sat, 24 Jan 2026 09:56:49 +0100 Subject: [PATCH 5/5] move out outline out of codepad.js --- .../bundle_resources/web/static/codepad.js | 367 ---------------- .../bundle_resources/web/static/outline.js | 405 ++++++++++++++++++ codepad/template.html | 1 + 3 files changed, 406 insertions(+), 367 deletions(-) create mode 100644 codepad/bundle_resources/web/static/outline.js diff --git a/codepad/bundle_resources/web/static/codepad.js b/codepad/bundle_resources/web/static/codepad.js index 66fe039..928bf06 100644 --- a/codepad/bundle_resources/web/static/codepad.js +++ b/codepad/bundle_resources/web/static/codepad.js @@ -311,373 +311,6 @@ function codepad_restart() { codepad_should_restart = true; } -function codepad_dump_hierarchy(silent) { - if (typeof Module === "undefined" || !Module.ccall) { - if (!scene_dump_missing_warned && !silent) { - console.warn("Scene dump unavailable: Module.ccall is missing."); - scene_dump_missing_warned = true; - } - return; - } - try { - codepad_set_dump_filter(); - var ptr = Module.ccall("CodepadSceneDump_DumpJson", "number", [], []); - if (!ptr) { - if (!silent) { - console.warn("Scene dump returned no data."); - } - return; - } - var json = Module.UTF8ToString(ptr); - var data = JSON.parse(json); - if (Array.isArray(data)) { - data = { _synthetic: true, children: data, props: { id: codepad_get_scene() || "scene" } }; - } - scene_hierarchy = data; - codepad_index_hierarchy(data); - var signature = codepad_build_structure_signature(data); - var structure_changed = signature !== scene_structure_signature; - scene_structure_signature = signature; - if (structure_changed || !document.getElementById("hierarchy-tree") || !document.getElementById("hierarchy-tree").hasChildNodes()) { - codepad_render_hierarchy(data); - } else { - codepad_render_properties(scene_node_index[scene_selected_path]); - } - if (!silent) { - console.log("Scene hierarchy:", data); - } - return data; - } catch (err) { - console.error("Scene dump failed:", err); - } -} - -function codepad_set_dump_filter() { - if (typeof Module === "undefined" || !Module.ccall) { - return; - } - var scene_id = codepad_get_scene(); - if (!scene_id || scene_id === scene_dump_filter) { - return; - } - Module.ccall("CodepadSceneDump_SetFilter", null, ["string"], [scene_id]); - scene_dump_filter = scene_id; -} - -function codepad_start_dump_loop() { - if (scene_dump_running) { - return; - } - scene_dump_running = true; - function tick() { - if (!scene_dump_running) { - return; - } - scene_dump_frame += 1; - if (scene_dump_frame % 2 === 0) { - codepad_dump_hierarchy(true); - } - requestAnimationFrame(tick); - } - requestAnimationFrame(tick); -} - -function codepad_build_structure_signature(node) { - var parts = []; - (function walk(current) { - if (!current) { - return; - } - if (current._synthetic) { - if (current.children) { - for (var i = 0; i < current.children.length; i++) { - walk(current.children[i]); - } - } - return; - } - parts.push(current._key || ""); - parts.push((current.props && current.props.id) || current.id || current.name || ""); - parts.push(current.type || ""); - var count = current.children ? current.children.length : 0; - parts.push(String(count)); - if (current.children) { - for (var i = 0; i < current.children.length; i++) { - walk(current.children[i]); - } - } - })(node); - return parts.join("|"); -} -function codepad_escape_html(value) { - return String(value) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/\"/g, """) - .replace(/'/g, "'"); -} - -function codepad_index_hierarchy(node) { - scene_node_index = {}; - if (!node) { - return; - } - (function walk(current, parentKey, index) { - if (!current) { - return; - } - if (current._synthetic) { - if (current.children) { - for (var i = 0; i < current.children.length; i++) { - walk(current.children[i], parentKey || "", i); - } - } - return; - } - var id = (current.props && current.props.id) || current.id || current.name || "node"; - var key = (parentKey ? parentKey + "/" : "") + id; - current._key = key; - scene_node_index[key] = current; - if (current.children) { - for (var i = 0; i < current.children.length; i++) { - walk(current.children[i], key, i); - } - } - })(node, "", 0); -} - -function codepad_build_tree_node(node) { - var wrapper = document.createElement("div"); - var hasChildren = node.children && node.children.length; - wrapper.className = "tree-node" + (hasChildren ? " is-expanded" : ""); - - var item = document.createElement("div"); - item.className = "tree-item"; - item.dataset.key = node._key || ""; - - var caret = document.createElement("span"); - caret.className = "tree-caret"; - if (!hasChildren) { - caret.style.visibility = "hidden"; - } - item.appendChild(caret); - - var label = document.createElement("span"); - label.className = "tree-label"; - label.textContent = (node.props && node.props.id) || node.id || node.name || "(unnamed)"; - item.appendChild(label); - - if (node.type) { - var meta = document.createElement("span"); - meta.className = "tree-meta"; - meta.textContent = node.type; - item.appendChild(meta); - } - - wrapper.appendChild(item); - - if (hasChildren) { - var children = document.createElement("div"); - children.className = "tree-children"; - for (var i = 0; i < node.children.length; i++) { - children.appendChild(codepad_build_tree_node(node.children[i])); - } - wrapper.appendChild(children); - } - - return wrapper; -} - -function codepad_render_hierarchy(tree) { - var container = document.getElementById("hierarchy-tree"); - if (!container) { - return; - } - container.innerHTML = ""; - if (!tree) { - return; - } - if (tree._synthetic && tree.children) { - for (var i = 0; i < tree.children.length; i++) { - container.appendChild(codepad_build_tree_node(tree.children[i])); - } - } else { - container.appendChild(codepad_build_tree_node(tree)); - } - codepad_bind_hierarchy_events(); - if (scene_selected_path && scene_node_index[scene_selected_path]) { - codepad_select_node(scene_selected_path); - } else if (tree._synthetic && tree.children && tree.children.length && tree.children[0]._key) { - codepad_select_node(tree.children[0]._key); - } else if (tree._key) { - codepad_select_node(tree._key); - } -} - -function codepad_bind_hierarchy_events() { - var container = document.getElementById("hierarchy-tree"); - if (!container || container._codepadBound) { - return; - } - container._codepadBound = true; - container.addEventListener("click", function (event) { - var caret = event.target.closest(".tree-caret"); - if (caret) { - var nodeElem = caret.closest(".tree-node"); - if (nodeElem && nodeElem.classList.contains("is-expanded")) { - nodeElem.classList.remove("is-expanded"); - nodeElem.classList.add("is-collapsed"); - } else if (nodeElem && nodeElem.classList.contains("is-collapsed")) { - nodeElem.classList.remove("is-collapsed"); - nodeElem.classList.add("is-expanded"); - } - event.stopPropagation(); - return; - } - var item = event.target.closest(".tree-item"); - if (!item) { - return; - } - var key = item.dataset.key; - if (!key) { - return; - } - codepad_select_node(key); - }); -} - -function codepad_select_node(key) { - var container = document.getElementById("hierarchy-tree"); - if (!container) { - return; - } - var previous = container.querySelector(".tree-item.is-selected"); - if (previous) { - previous.classList.remove("is-selected"); - } - var next = container.querySelector('.tree-item[data-key="' + key + '"]'); - if (next) { - next.classList.add("is-selected"); - } - scene_selected_path = key; - codepad_render_properties(scene_node_index[key]); -} - -function codepad_format_prop_value(value) { - if (value === null || value === undefined) { - return "null"; - } - if (Array.isArray(value)) { - return "[" + value.map(codepad_format_prop_value).join(", ") + "]"; - } - if (typeof value === "object") { - try { - return JSON.stringify(value); - } catch (err) { - return String(value); - } - } - return String(value); -} - -function codepad_render_properties(node) { - var container = document.getElementById("properties-list"); - if (!container) { - return; - } - if (!node) { - container.innerHTML = ""; - container._codepadNodeKey = null; - container._codepadKeySig = null; - container._codepadValueEls = null; - container._codepadRowEls = null; - container._codepadHeaderEl = null; - return; - } - - var props = {}; - if (node.props) { - for (var key in node.props) { - if (node.props.hasOwnProperty(key)) { - props[key] = node.props[key]; - } - } - } - if (!props.id) { - props.id = (node.props && node.props.id) || node.id || node.name || node._key || "node"; - } - - var priority = ["id", "name", "path", "url", "position", "rotation", "scale", "size", "pivot", "anchorPoint", "visible", "enabled", "layer"]; - var keys = Object.keys(props).sort(); - keys.sort(function (a, b) { - var ai = priority.indexOf(a); - var bi = priority.indexOf(b); - if (ai === -1 && bi === -1) { - return a.localeCompare(b); - } - if (ai === -1) { - return 1; - } - if (bi === -1) { - return -1; - } - return ai - bi; - }); - - var node_key = node._key || props.id || "node"; - container._codepadNodeKey = node_key; - - if (!container._codepadHeaderEl) { - var header = document.createElement("div"); - header.className = "properties-header"; - container.appendChild(header); - container._codepadHeaderEl = header; - } - container._codepadHeaderEl.textContent = props.id || "Node"; - - if (!container._codepadRowEls) { - container._codepadRowEls = {}; - } - if (!container._codepadValueEls) { - container._codepadValueEls = {}; - } - - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var row = container._codepadRowEls[key]; - var valueSpan = container._codepadValueEls[key]; - if (!row || !valueSpan) { - row = document.createElement("div"); - row.className = "prop-row"; - - var keySpan = document.createElement("span"); - keySpan.className = "prop-key"; - keySpan.textContent = key; - - valueSpan = document.createElement("span"); - valueSpan.className = "prop-value"; - - row.appendChild(keySpan); - row.appendChild(valueSpan); - container.appendChild(row); - - container._codepadRowEls[key] = row; - container._codepadValueEls[key] = valueSpan; - } - row.style.display = "grid"; - valueSpan.textContent = codepad_format_prop_value(props[key]); - } - - for (var existing in container._codepadRowEls) { - if (container._codepadRowEls.hasOwnProperty(existing)) { - if (keys.indexOf(existing) === -1) { - container._codepadRowEls[existing].style.display = "none"; - } - } - } -} - function codepad_get_code(i) { if (codepad_sessions[i - 1]) { if (EditSession !== undefined) { diff --git a/codepad/bundle_resources/web/static/outline.js b/codepad/bundle_resources/web/static/outline.js new file mode 100644 index 0000000..1ac095b --- /dev/null +++ b/codepad/bundle_resources/web/static/outline.js @@ -0,0 +1,405 @@ +/*jshint esversion: 6 */ + +/** + * Fetch the scene hierarchy JSON from the native extension and update UI. + */ +function codepad_dump_hierarchy(silent) { + if (typeof Module === "undefined" || !Module.ccall) { + if (!scene_dump_missing_warned && !silent) { + console.warn("Scene dump unavailable: Module.ccall is missing."); + scene_dump_missing_warned = true; + } + return; + } + try { + codepad_set_dump_filter(); + var ptr = Module.ccall("CodepadSceneDump_DumpJson", "number", [], []); + if (!ptr) { + if (!silent) { + console.warn("Scene dump returned no data."); + } + return; + } + var json = Module.UTF8ToString(ptr); + var data = JSON.parse(json); + if (Array.isArray(data)) { + data = { _synthetic: true, children: data, props: { id: codepad_get_scene() || "scene" } }; + } + scene_hierarchy = data; + codepad_index_hierarchy(data); + var signature = codepad_build_structure_signature(data); + var structure_changed = signature !== scene_structure_signature; + scene_structure_signature = signature; + if (structure_changed || !document.getElementById("hierarchy-tree") || !document.getElementById("hierarchy-tree").hasChildNodes()) { + codepad_render_hierarchy(data); + } else { + codepad_render_properties(scene_node_index[scene_selected_path]); + } + if (!silent) { + console.log("Scene hierarchy:", data); + } + return data; + } catch (err) { + console.error("Scene dump failed:", err); + } +} + +/** + * Update native dump filter to limit data to the active scene. + */ +function codepad_set_dump_filter() { + if (typeof Module === "undefined" || !Module.ccall) { + return; + } + var scene_id = codepad_get_scene(); + if (!scene_id || scene_id === scene_dump_filter) { + return; + } + Module.ccall("CodepadSceneDump_SetFilter", null, ["string"], [scene_id]); + scene_dump_filter = scene_id; +} + +/** + * Start the per-frame (every other frame) hierarchy polling loop. + */ +function codepad_start_dump_loop() { + if (scene_dump_running) { + return; + } + scene_dump_running = true; + function tick() { + if (!scene_dump_running) { + return; + } + scene_dump_frame += 1; + if (scene_dump_frame % 2 === 0) { + codepad_dump_hierarchy(true); + } + requestAnimationFrame(tick); + } + requestAnimationFrame(tick); +} + +/** + * Build a signature string for detecting hierarchy structure changes. + */ +function codepad_build_structure_signature(node) { + var parts = []; + (function walk(current) { + if (!current) { + return; + } + if (current._synthetic) { + if (current.children) { + for (var i = 0; i < current.children.length; i++) { + walk(current.children[i]); + } + } + return; + } + parts.push(current._key || ""); + parts.push((current.props && current.props.id) || current.id || current.name || ""); + parts.push(current.type || ""); + var count = current.children ? current.children.length : 0; + parts.push(String(count)); + if (current.children) { + for (var i = 0; i < current.children.length; i++) { + walk(current.children[i]); + } + } + })(node); + return parts.join("|"); +} + +/** + * Escape a string for safe HTML insertion. + */ +function codepad_escape_html(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); +} + +/** + * Index hierarchy nodes by a stable key path for quick lookup. + */ +function codepad_index_hierarchy(node) { + scene_node_index = {}; + if (!node) { + return; + } + (function walk(current, parentKey, index) { + if (!current) { + return; + } + if (current._synthetic) { + if (current.children) { + for (var i = 0; i < current.children.length; i++) { + walk(current.children[i], parentKey || "", i); + } + } + return; + } + var id = (current.props && current.props.id) || current.id || current.name || "node"; + var key = (parentKey ? parentKey + "/" : "") + id; + current._key = key; + scene_node_index[key] = current; + if (current.children) { + for (var i = 0; i < current.children.length; i++) { + walk(current.children[i], key, i); + } + } + })(node, "", 0); +} + +/** + * Build a DOM subtree for a hierarchy node. + */ +function codepad_build_tree_node(node) { + var wrapper = document.createElement("div"); + var hasChildren = node.children && node.children.length; + wrapper.className = "tree-node" + (hasChildren ? " is-expanded" : ""); + + var item = document.createElement("div"); + item.className = "tree-item"; + item.dataset.key = node._key || ""; + + var caret = document.createElement("span"); + caret.className = "tree-caret"; + if (!hasChildren) { + caret.style.visibility = "hidden"; + } + item.appendChild(caret); + + var label = document.createElement("span"); + label.className = "tree-label"; + label.textContent = (node.props && node.props.id) || node.id || node.name || "(unnamed)"; + item.appendChild(label); + + if (node.type) { + var meta = document.createElement("span"); + meta.className = "tree-meta"; + meta.textContent = node.type; + item.appendChild(meta); + } + + wrapper.appendChild(item); + + if (hasChildren) { + var children = document.createElement("div"); + children.className = "tree-children"; + for (var i = 0; i < node.children.length; i++) { + children.appendChild(codepad_build_tree_node(node.children[i])); + } + wrapper.appendChild(children); + } + + return wrapper; +} + +/** + * Render the hierarchy tree and update selection. + */ +function codepad_render_hierarchy(tree) { + var container = document.getElementById("hierarchy-tree"); + if (!container) { + return; + } + container.innerHTML = ""; + if (!tree) { + return; + } + if (tree._synthetic && tree.children) { + for (var i = 0; i < tree.children.length; i++) { + container.appendChild(codepad_build_tree_node(tree.children[i])); + } + } else { + container.appendChild(codepad_build_tree_node(tree)); + } + codepad_bind_hierarchy_events(); + if (scene_selected_path && scene_node_index[scene_selected_path]) { + codepad_select_node(scene_selected_path); + } else if (tree._synthetic && tree.children && tree.children.length && tree.children[0]._key) { + codepad_select_node(tree.children[0]._key); + } else if (tree._key) { + codepad_select_node(tree._key); + } +} + +/** + * Bind click handlers for expand/collapse and selection. + */ +function codepad_bind_hierarchy_events() { + var container = document.getElementById("hierarchy-tree"); + if (!container || container._codepadBound) { + return; + } + container._codepadBound = true; + container.addEventListener("click", function (event) { + var caret = event.target.closest(".tree-caret"); + if (caret) { + var nodeElem = caret.closest(".tree-node"); + if (nodeElem && nodeElem.classList.contains("is-expanded")) { + nodeElem.classList.remove("is-expanded"); + nodeElem.classList.add("is-collapsed"); + } else if (nodeElem && nodeElem.classList.contains("is-collapsed")) { + nodeElem.classList.remove("is-collapsed"); + nodeElem.classList.add("is-expanded"); + } + event.stopPropagation(); + return; + } + var item = event.target.closest(".tree-item"); + if (!item) { + return; + } + var key = item.dataset.key; + if (!key) { + return; + } + codepad_select_node(key); + }); +} + +/** + * Select a node by key and show its properties. + */ +function codepad_select_node(key) { + var container = document.getElementById("hierarchy-tree"); + if (!container) { + return; + } + var previous = container.querySelector(".tree-item.is-selected"); + if (previous) { + previous.classList.remove("is-selected"); + } + var next = container.querySelector('.tree-item[data-key="' + key + '"]'); + if (next) { + next.classList.add("is-selected"); + } + scene_selected_path = key; + codepad_render_properties(scene_node_index[key]); +} + +/** + * Format a property value into a readable string. + */ +function codepad_format_prop_value(value) { + if (value === null || value === undefined) { + return "null"; + } + if (Array.isArray(value)) { + return "[" + value.map(codepad_format_prop_value).join(", ") + "]"; + } + if (typeof value === "object") { + try { + return JSON.stringify(value); + } catch (err) { + return String(value); + } + } + return String(value); +} + +/** + * Render or update the properties list for a selected node. + */ +function codepad_render_properties(node) { + var container = document.getElementById("properties-list"); + if (!container) { + return; + } + if (!node) { + container.innerHTML = ""; + container._codepadNodeKey = null; + container._codepadKeySig = null; + container._codepadValueEls = null; + container._codepadRowEls = null; + container._codepadHeaderEl = null; + return; + } + + var props = {}; + if (node.props) { + for (var key in node.props) { + if (node.props.hasOwnProperty(key)) { + props[key] = node.props[key]; + } + } + } + if (!props.id) { + props.id = (node.props && node.props.id) || node.id || node.name || node._key || "node"; + } + + var priority = ["id", "name", "path", "url", "position", "rotation", "scale", "size", "pivot", "anchorPoint", "visible", "enabled", "layer"]; + var keys = Object.keys(props).sort(); + keys.sort(function (a, b) { + var ai = priority.indexOf(a); + var bi = priority.indexOf(b); + if (ai === -1 && bi === -1) { + return a.localeCompare(b); + } + if (ai === -1) { + return 1; + } + if (bi === -1) { + return -1; + } + return ai - bi; + }); + + var node_key = node._key || props.id || "node"; + container._codepadNodeKey = node_key; + + if (!container._codepadHeaderEl) { + var header = document.createElement("div"); + header.className = "properties-header"; + container.appendChild(header); + container._codepadHeaderEl = header; + } + container._codepadHeaderEl.textContent = props.id || "Node"; + + if (!container._codepadRowEls) { + container._codepadRowEls = {}; + } + if (!container._codepadValueEls) { + container._codepadValueEls = {}; + } + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var row = container._codepadRowEls[key]; + var valueSpan = container._codepadValueEls[key]; + if (!row || !valueSpan) { + row = document.createElement("div"); + row.className = "prop-row"; + + var keySpan = document.createElement("span"); + keySpan.className = "prop-key"; + keySpan.textContent = key; + + valueSpan = document.createElement("span"); + valueSpan.className = "prop-value"; + + row.appendChild(keySpan); + row.appendChild(valueSpan); + container.appendChild(row); + + container._codepadRowEls[key] = row; + container._codepadValueEls[key] = valueSpan; + } + row.style.display = "grid"; + valueSpan.textContent = codepad_format_prop_value(props[key]); + } + + for (var existing in container._codepadRowEls) { + if (container._codepadRowEls.hasOwnProperty(existing)) { + if (keys.indexOf(existing) === -1) { + container._codepadRowEls[existing].style.display = "none"; + } + } + } +} diff --git a/codepad/template.html b/codepad/template.html index 51e850a..ae0c063 100644 --- a/codepad/template.html +++ b/codepad/template.html @@ -117,6 +117,7 @@ +