diff --git a/leenkx/Sources/leenkx/logicnode/SetFirstPersonControllerNode.hx b/leenkx/Sources/leenkx/logicnode/SetFirstPersonControllerNode.hx index 236621a2..83e8dcf3 100644 --- a/leenkx/Sources/leenkx/logicnode/SetFirstPersonControllerNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetFirstPersonControllerNode.hx @@ -10,6 +10,7 @@ class SetFirstPersonControllerNode extends LogicNode { } override function run(from: Int): Void { + #if lnx_physics // Control de las var de FirstPersonController... // Control FirstPersonController var @@ -60,6 +61,7 @@ class SetFirstPersonControllerNode extends LogicNode { // Alert the user if they do not have the trait assigning to the object. trace("ERROR: The object '" + object.name + "' does not have the FirstPersonController script assigning(assign it from (Object->add trait->bundle))."); } + #end runOutput(0); } diff --git a/leenkx/Sources/leenkx/logicnode/SetOverheadPersonControllerNode.hx b/leenkx/Sources/leenkx/logicnode/SetOverheadPersonControllerNode.hx index 539cff2d..55828aa0 100644 --- a/leenkx/Sources/leenkx/logicnode/SetOverheadPersonControllerNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetOverheadPersonControllerNode.hx @@ -10,6 +10,7 @@ class SetOverheadPersonControllerNode extends LogicNode { } override function run(from: Int): Void { + #if lnx_physics // Control de las var de OverheadPersonController... // Control OverheadPersonController var @@ -58,6 +59,7 @@ class SetOverheadPersonControllerNode extends LogicNode { // Alert the user if they do not have the trait assigning to the object.. trace("ERROR: The object '" + objectTrait.name + "' does not have the OverheadPersonController script assigning(assign it from (Object->add trait->bundle))."); } + #end runOutput(0); } diff --git a/leenkx/Sources/leenkx/trait/internal/LivePatch.hx b/leenkx/Sources/leenkx/trait/internal/LivePatch.hx index db8f8ce5..cbabb280 100644 --- a/leenkx/Sources/leenkx/trait/internal/LivePatch.hx +++ b/leenkx/Sources/leenkx/trait/internal/LivePatch.hx @@ -27,7 +27,7 @@ class LivePatch extends iron.Trait { var id = Std.parseInt(lines[0]); if (id > patchId) { patchId = id; - js.Lib.eval(lines[1]); + js.Syntax.code("var iron = {0}, LivePatch = {1}; eval({2})", Bridge, LivePatch, lines[1]); } }); } diff --git a/leenkx/blender/lnx/handlers.py b/leenkx/blender/lnx/handlers.py index 89c6f2fe..295f657f 100644 --- a/leenkx/blender/lnx/handlers.py +++ b/leenkx/blender/lnx/handlers.py @@ -67,10 +67,11 @@ def on_depsgraph_update_post(self): # Send last operator to Krom wrd = bpy.data.worlds['Lnx'] - if state.proc_play is not None and state.target == 'krom' and wrd.lnx_live_patch: - ops = bpy.context.window_manager.operators - if len(ops) > 0 and ops[-1] is not None: - live_patch.on_operator(ops[-1].bl_idname) + if (state.proc_play is not None or make._viewport_processes) and state.target == 'krom' and wrd.lnx_live_patch: + if not live_patch.on_depsgraph_update(depsgraph.updates): + ops = bpy.context.window_manager.operators + if len(ops) > 0 and ops[-1] is not None: + live_patch.on_operator(ops[-1].bl_idname) # Hacky solution to update leenkx props after operator executions. # bpy.context.active_operator doesn't always exist, in some cases diff --git a/leenkx/blender/lnx/live_patch.py b/leenkx/blender/lnx/live_patch.py index 7ca57ff3..3e32f33d 100644 --- a/leenkx/blender/lnx/live_patch.py +++ b/leenkx/blender/lnx/live_patch.py @@ -32,6 +32,9 @@ else: __running = False """Whether live patch is currently active""" + scene_cache = {} + """Keys: (event_id, object_name) -> cached value tuple""" + # Any object can act as a message bus owner msgbus_owner = object() @@ -65,6 +68,62 @@ def stop(): bpy.msgbus.clear_by_owner(msgbus_owner) +def get_obj_transforms(obj): + loc = (round(obj.location.x, 6), round(obj.location.y, 6), round(obj.location.z, 6)) + rot = obj.rotation_euler.to_quaternion() + rot = (round(rot.x, 6), round(rot.y, 6), round(rot.z, 6), round(rot.w, 6)) + scale = (round(obj.scale.x, 6), round(obj.scale.y, 6), round(obj.scale.z, 6)) + return loc, rot, scale + + +def get_light_props(obj): + light = obj.data + color = (round(light.color[0], 6), round(light.color[1], 6), round(light.color[2], 6)) + energy = round(light.energy, 6) + return color, energy + +DEPSGRAPH_HANDLERS = { + 'is_updated_transform': [ + ('obj_location', lambda obj: get_obj_transforms(obj)[0]), + ('obj_rotation', lambda obj: get_obj_transforms(obj)[1]), + ('obj_scale', lambda obj: get_obj_transforms(obj)[2]), + ], +} + + +def on_depsgraph_update(updates): + if not __running: + return False + + changed = False + + for update in updates: + uid = update.id + + if isinstance(uid, bpy.types.Object): + name = uid.name + + if update.is_updated_transform: + for event_id, extractor in DEPSGRAPH_HANDLERS['is_updated_transform']: + current = extractor(uid) + cache_key = (event_id, name) + if current != scene_cache.get(cache_key): + scene_cache[cache_key] = current + send_event(event_id) + changed = True + + if update.is_updated_shading and isinstance(uid.data, bpy.types.Light): + color, energy = get_light_props(uid) + for event_id, current in (('light_color', color), ('light_energy', energy)): + cache_key = (event_id, uid.data.name) + if current != scene_cache.get(cache_key): + scene_cache[cache_key] = current + send_event(event_id) + changed = True + + return changed + + def patch_export(): """Re-export the current scene and update the game accordingly.""" if not __running or state.proc_build is not None: diff --git a/leenkx/blender/lnx/make.py b/leenkx/blender/lnx/make.py index b2943ee4..f780d3cb 100644 --- a/leenkx/blender/lnx/make.py +++ b/leenkx/blender/lnx/make.py @@ -521,12 +521,17 @@ def compile(assets_only=False): else: log.error('Build failed') -def build(target, is_play=False, is_publish=False, is_export=False): +def build(target, is_play=False, is_publish=False, is_export=False, is_viewport=False, viewport_width=1920, viewport_height=1080): global profile_time profile_time = time.time() wrd = bpy.data.worlds['Lnx'] + state.is_viewport = is_viewport + if is_viewport: + state.viewport_width = viewport_width + state.viewport_height = viewport_height + if is_play and wrd.lnx_runtime == 'Hashlink': current_os = lnx.utils.get_os() if current_os == 'win': @@ -690,7 +695,11 @@ def run_viewport_runtime(viewport_id, width=1920, height=1080): if not os.path.exists(path + '/krom.js'): log.warn(f'Krom build not found at {path}/krom.js - build project first') return None, None - + + if wrd.lnx_live_patch: + live_patch.start() + open(path + '/krom.patch', 'w', encoding='utf-8').close() + os.chdir(krom_location) cmd = [krom_path, path, path_resources] @@ -725,95 +734,6 @@ _viewport_build_in_progress = False _viewport_pending_launches = [] # List of (viewport_id, width, height) tuples _viewport_proc_build = None # Separate process tracker for viewport builds -def build_viewport(viewport_id, width=1920, height=1080): - """Build project for viewport""" - global _viewport_build_in_progress, _viewport_pending_launches, profile_time, scripts_mtime - - wrd = bpy.data.worlds.get('Lnx') - if not wrd: - log.warn('No Lnx world found - cannot build for viewport') - return - - krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js' - if os.path.exists(krom_js_path) and not wrd.lnx_recompile: - log.info(f'Using cached viewport build for {viewport_id}') - run_viewport_runtime(viewport_id, width, height) - return - - pending_entry = (viewport_id, width, height) - if pending_entry not in _viewport_pending_launches: - _viewport_pending_launches.append(pending_entry) - log.info(f'Queued viewport {viewport_id} for launch after build') - - # If a build is already in progress and there is an actual build process, wait - # _viewport_proc_build checks viewport ,not state.proc_build (which is for play button) - if _viewport_build_in_progress: - if _viewport_proc_build is not None and _viewport_proc_build.poll() is None: - log.info(f'Build in progress, viewport {viewport_id} will launch when ready') - return - else: - log.info(f'Resetting stale viewport build state') - _viewport_build_in_progress = False - - _viewport_build_in_progress = True - profile_time = time.time() - - log.info(f'Starting viewport build for {len(_viewport_pending_launches)} viewport(s)') - - # Set viewport mode flags but not the is_play flag meant for external launcher - state.is_viewport = True - state.viewport_width = width - state.viewport_height = height - state.target = 'krom' - state.is_play = False # NOT play mode - state.is_publish = False - state.is_export = False - - # Recompile detection (matching play() behavior) - if not wrd.lnx_cache_build or \ - not os.path.isfile(krom_js_path) or \ - assets.khafile_defs_last != assets.khafile_defs or \ - state.last_target != state.target: - wrd.lnx_recompile = True - - state.last_target = state.target - - # Trait sources modified - state.mod_scripts = [] - script_path = lnx.utils.get_fp() + '/Sources/' + lnx.utils.safestr(wrd.lnx_project_package) - if os.path.isdir(script_path): - new_mtime = scripts_mtime - for fn in glob.iglob(os.path.join(script_path, '**', '*.hx'), recursive=True): - mtime = os.path.getmtime(fn) - if scripts_mtime < mtime: - lnx.utils.fetch_script_props(fn) - fn = fn.split('Sources/')[1] - fn = fn[:-3] #.hx - fn = fn.replace('/', '.') - state.mod_scripts.append(fn) - wrd.lnx_recompile = True - if new_mtime < mtime: - new_mtime = mtime - scripts_mtime = new_mtime - if len(state.mod_scripts) > 0: - lnx.utils.fetch_trait_props() - - log.clear(clear_warnings=True, clear_errors=True) - - sdk_path = lnx.utils.get_sdk_path() - fp = lnx.utils.get_fp() - os.chdir(fp) - - sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package) - if not os.path.exists(sources_path): - os.makedirs(sources_path) - - log.info('Exporting scene data...') - export_data(fp, sdk_path) - - log.info('Starting Krom compilation for viewport...') - compile_viewport(assets_only=(not wrd.lnx_recompile)) - def compile_viewport(assets_only=False): """Compile for viewport mode using separate process tracking.""" global _viewport_proc_build @@ -886,8 +806,8 @@ def viewport_build_done(): def play_viewport(viewport_id, width=1920, height=1080): """Launch Krom in a viewport""" - global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build, scripts_mtime - + global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build + if not viewport_id: log.error('No viewport_id: play_viewport requires an id') return @@ -895,7 +815,7 @@ def play_viewport(viewport_id, width=1920, height=1080): if 'Lnx' not in bpy.data.worlds: log.error('No Lnx world found - cannot start viewport server') return - + wrd = bpy.data.worlds['Lnx'] krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js' @@ -912,52 +832,11 @@ def play_viewport(viewport_id, width=1920, height=1080): log.info(f'Build in progress, viewport {viewport_id} will launch when ready') return else: - _viewport_build_in_progress = False # Reset stale state + _viewport_build_in_progress = False _viewport_build_in_progress = True - sdk_path = lnx.utils.get_sdk_path() - fp = lnx.utils.get_fp() - os.chdir(fp) - - sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package) - if not os.path.exists(sources_path): - os.makedirs(sources_path) - - # export data same as play() but without setting state.is_play - state.is_viewport = True - state.target = 'krom' - state.is_publish = False - state.is_export = False - - if not wrd.lnx_cache_build or \ - not os.path.isfile(krom_js_path) or \ - assets.khafile_defs_last != assets.khafile_defs or \ - state.last_target != state.target: - wrd.lnx_recompile = True - - state.last_target = state.target - - state.mod_scripts = [] - script_path = lnx.utils.get_fp() + '/Sources/' + lnx.utils.safestr(wrd.lnx_project_package) - if os.path.isdir(script_path): - new_mtime = scripts_mtime - for fn in glob.iglob(os.path.join(script_path, '**', '*.hx'), recursive=True): - mtime = os.path.getmtime(fn) - if scripts_mtime < mtime: - lnx.utils.fetch_script_props(fn) - fn = fn.split('Sources/')[1] - fn = fn[:-3] #.hx - fn = fn.replace('/', '.') - state.mod_scripts.append(fn) - wrd.lnx_recompile = True - if new_mtime < mtime: - new_mtime = mtime - scripts_mtime = new_mtime - if len(state.mod_scripts) > 0: - lnx.utils.fetch_trait_props() - - export_data(fp, sdk_path) + build(target='krom', is_viewport=True, viewport_width=width, viewport_height=height) compile_viewport(assets_only=(not wrd.lnx_recompile)) diff --git a/leenkx/blender/lnx/render_engine.py b/leenkx/blender/lnx/render_engine.py index a77e5814..6b36586d 100644 --- a/leenkx/blender/lnx/render_engine.py +++ b/leenkx/blender/lnx/render_engine.py @@ -101,70 +101,9 @@ MOUSE_BUTTON_LEFT = 0 MOUSE_BUTTON_RIGHT = 1 MOUSE_BUTTON_MIDDLE = 2 -_rendered_mode_pending = False -_rendered_mode_retries = 0 _msgbus_owner = object() _make_module = None -@bpy.app.handlers.persistent -def _on_depsgraph_update(scene, depsgraph): - """Called on depsgraph update - check if render engine changed to Krom.""" - try: - if scene.render.engine == 'KROM_VIEWPORT': - for window in bpy.context.window_manager.windows: - for area in window.screen.areas: - if area.type == 'VIEW_3D': - for space in area.spaces: - if space.type == 'VIEW_3D': - if space.shading.type != 'RENDERED': - space.shading.type = 'RENDERED' - area.tag_redraw() - return - except: - pass - -def _deferred_set_rendered_mode(): - """Module-level timer callback to set rendered mode.""" - global _rendered_mode_pending, _rendered_mode_retries - try: - current_engine = bpy.context.scene.render.engine - - for window in bpy.context.window_manager.windows: - screen = window.screen - for area in screen.areas: - if area.type == 'VIEW_3D': - for space in area.spaces: - if space.type == 'VIEW_3D': - current_shading = space.shading.type - - if current_shading == 'RENDERED': - _rendered_mode_pending = False - _rendered_mode_retries = 0 - return None - - space.shading.type = 'RENDERED' - area.tag_redraw() - - _rendered_mode_pending = False - _rendered_mode_retries = 0 - return None - - _rendered_mode_retries += 1 - if _rendered_mode_retries < 10: - print(f"Viewport VIEW_3D not found, retrying... ({_rendered_mode_retries})") - return 0.2 - except Exception as e: - print(f"Could not set rendered mode: {e}") - import traceback - traceback.print_exc() - _rendered_mode_retries += 1 - if _rendered_mode_retries < 10: - return 0.2 - _rendered_mode_pending = False - _rendered_mode_retries = 0 - return None - - class KromViewportEngine(bpy.types.RenderEngine): """Leenkx render engine that displays Krom runtime output in viewport.""" @@ -190,12 +129,12 @@ class KromViewportEngine(bpy.types.RenderEngine): self._batch = None self._batch_dims = None self._viewport_init_done = True - self._rendered_mode_set = False self._last_shading_mode = None self._viewport_id = None self._shmem_name = None self._krom_proc = None self._krom_launch_attempted = False + self._viewport_stopped = False self._pending_camera_sync = False self._input_enabled_state = None self._last_region_dims = None @@ -203,24 +142,6 @@ class KromViewportEngine(bpy.types.RenderEngine): self._engine_id = id(self) # print(f"New engine instance created: {self._engine_id}") - def _set_rendered_mode(self, context): - if self._rendered_mode_set: - return - try: - space = context.space_data - if space and hasattr(space, 'shading'): - current = space.shading.type - if current != 'RENDERED': - space.shading.type = 'RENDERED' - self._rendered_mode_set = True - # Disable viewport overlay so Krom output is unobstructed - if space and hasattr(space, 'overlay'): - if not hasattr(self, '_overlay_saved'): - self._overlay_saved = space.overlay.show_overlays - space.overlay.show_overlays = False - except Exception as e: - print(f"Failed to set rendered mode: {e}") - def _restore_overlay(self, context=None): try: if not hasattr(self, '_overlay_saved'): @@ -252,15 +173,15 @@ class KromViewportEngine(bpy.types.RenderEngine): current_mode = space.shading.type if self._last_shading_mode == 'SOLID' and current_mode == 'RENDERED': + self._viewport_stopped = False self._initialized = False - self._rendered_mode_set = False self._krom_launch_attempted = False self._cleanup_shared_memory() self._pending_camera_sync = True if self._last_shading_mode == 'RENDERED' and current_mode != 'RENDERED': - self._rendered_mode_set = False - # keep krom running in solid mode for switching back ti rendered + # keep krom running in solid mode for switching back to rendered + pass self._last_shading_mode = current_mode except Exception as e: @@ -337,13 +258,13 @@ class KromViewportEngine(bpy.types.RenderEngine): return True def _stop_krom_process(self): - if self._krom_proc: + import lnx.make as make + if self._viewport_id and self._viewport_id in make._viewport_processes: try: - import lnx.make as make make.stop_viewport_runtime(self._viewport_id) except: pass - self._krom_proc = None + self._krom_proc = None self._krom_launch_attempted = False self._initialized = False self._cleanup_shared_memory() @@ -946,6 +867,10 @@ class KromViewportEngine(bpy.types.RenderEngine): if state.is_exporting: return + if self._viewport_stopped: + self._draw_placeholder(context) + return + self._ensure_initialized() self._handle_mode_switch(context) @@ -984,8 +909,6 @@ class KromViewportEngine(bpy.types.RenderEngine): if self._viewport_id in _active_krom_engines: del _active_krom_engines[self._viewport_id] - self._set_rendered_mode(context) - # TODO: input forwarding when viewport is active _active_krom_engine = self # legacy single-engine reference @@ -1375,6 +1298,7 @@ class KROM_OT_stop_viewport(bpy.types.Operator): if viewport_id in _active_krom_engines: engine = _active_krom_engines[viewport_id] try: + engine._viewport_stopped = True engine._stop_krom_process() engine._restore_overlay(context) except: @@ -1422,8 +1346,6 @@ def register(): for panel in get_panels(): panel.COMPAT_ENGINES.add('KROM_VIEWPORT') - if _on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: - bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update) atexit.register(_cleanup_krom_on_exit) @@ -1435,9 +1357,6 @@ def unregister(): _active_viewport_id = None _input_operator_running = False - if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: - bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update) - _cleanup_krom_on_exit() try: