diff --git a/codepad/bundle_resources/web/static/codepad.css b/codepad/bundle_resources/web/static/codepad.css index 69a5cfe..0c21241 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,121 @@ 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; +} + +.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 { } @@ -235,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 e22fb5f..928bf06 100644 --- a/codepad/bundle_resources/web/static/codepad.js +++ b/codepad/bundle_resources/web/static/codepad.js @@ -15,6 +15,14 @@ var scenes = []; var project_info = {}; 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) @@ -87,14 +95,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: [50, 50], + minSize: [80, 80] + }); + } if (callback) { callback(); @@ -194,6 +227,9 @@ function codepad_change_scene() { break; } } + scene_structure_signature = null; + scene_selected_path = null; + codepad_set_dump_filter(); } @@ -234,6 +270,10 @@ function codepad_ready(scenes_json, project_json, engine_json) { codepad_trigger_url_check(); codepad_change_scene(); + setTimeout(function () { + codepad_dump_hierarchy(true); + codepad_start_dump_loop(); + }, 0); } /** @@ -447,12 +487,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)); } } @@ -471,8 +522,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/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/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 92fe3f7..ae0c063 100644 --- a/codepad/template.html +++ b/codepad/template.html @@ -32,12 +32,12 @@
-
-
-
-
+
+
+
+
- +
@@ -49,12 +49,27 @@
-
-
- +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+ +
- -
@@ -69,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); @@ -88,6 +117,7 @@ + diff --git a/game.project b/game.project index 09c2690..b5a0bc4 100644 --- a/game.project +++ b/game.project @@ -1,9 +1,9 @@ [project] title = DefoldCodePad -version = 0.1 +version = 0.2 bundle_resources = /codepad/bundle_resources/ bundle_exclude_resources = -custom_resources = main/scripts +custom_resources = /main/scripts [bootstrap] main_collection = /main/main.collectionc @@ -28,4 +28,10 @@ shared_state = 1 [html5] 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/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/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" "" } 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 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..50504c3 --- /dev/null +++ b/scene_dump/src/scene_dump.cpp @@ -0,0 +1,450 @@ +#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; + std::string g_FilterId; + + 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 || strcmp(key, "resource") == 0 || strcmp(key, "script_id") == 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 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; + const char* type_str = type_hash ? dmHashReverseSafe64(type_hash) : 0; + + if (IsCollectionProxyType(type_str) && MatchesFilter(name_str, filter)) + { + *out = *node; + return true; + } + + 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); + + const char* type_str = type_hash ? dmHashReverseSafe64(type_hash) : 0; + if (IsCollectionProxyType(type_str)) + { + *out = *node; + return true; + } + + dmGameObject::SceneNodeIterator it = dmGameObject::TraverseIterateChildren(node); + while (dmGameObject::TraverseIterateNext(&it)) + { + dmGameObject::SceneNode child = it.m_Node; + if (FindFirstCollectionProxy(&child, out)) + { + return true; + } + } + 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') + { + 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, "type", type_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); + bool first_child = true; + while (dmGameObject::TraverseIterateNext(&it)) + { + if (!first_child) + { + out.push_back(','); + } + first_child = false; + DumpNode(out, &it.m_Node); + } + 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); + 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(); + } +} + +#if defined(DM_PLATFORM_HTML5) +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) +{ + 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)