fix(editor): Play runs the scene tab that is open - #45
Closed
pythonlearner1025 wants to merge 2 commits into
Closed
pythonlearner1025 wants to merge 2 commits into
pythonlearner1025 wants to merge 2 commits into
Conversation
The owner had assets/weapons-lab.scene.gltf open in a tab, pressed Play, and the main scene ran. Pass 1a pinned Play to the main scene by the plan's own decision. A scene on screen is the scene you expect to play, so the rule is now the active document. - startRunMode takes store.active when it is a SceneDocument, else store.mainScene. - beforeRun names the scene the run borrowed, and stopRunMode restores that one. - The plan's 4.4 Play paragraph records the new rule and why. Proved headless on a copy of the terminator project: the weapons lab tab now runs the weapons lab, the main scene tab and an object or texture tab run the main scene, and Stop puts the scene that ran back with its name, its saved hash and its unsaved edit. typecheck, lint, kite3d (9 tests) and test:scripts pass.
Play from an object, material or texture tab has to switch to the main scene to run it. Stop left the editor on that scene. The tab the user pressed Play on is the tab they expect back. - beforeRun records store.activeId before the activate that Play does. - stopRunMode activates it again once the scene is restored. - activateForPlay already no-ops on a tab that is gone or already on screen, so a scene tab stays where it is and a closed tab leaves the scene on screen. Proved headless on a copy of the terminator project: from an object tab and from a texture tab the main scene runs and Stop brings the tab back with its own tree on the viewport; from the weapons lab tab and from the main scene tab Stop stays put. One new test guards it. typecheck, lint, kite3d (10 tests) and test:scripts pass.
pythonlearner1025
force-pushed
the
fix/play-active-scene
branch
from
September 17, 2026 00:43
cbb4b21 to
6f3ede3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What part this touches
The Kite3D editor holds many open documents and one 3D viewport. A document is one file on the viewport: a scene, an object asset, a material, or a texture.
DocumentStoreowns the list, which one is active, and the tab strip that draws them.PlayModeHelperowns Play. It puts a scene on the viewport. It exports an in-memory snapshot of that scene. It then calls the engine'sstartGameon the same viewer. Stop puts the authored scene back.There are two ways a project runs. The editor's Play calls
startGame(viewer, project, options)on a viewer that already holds its scene. The published game callscreateGame. That one builds its own viewer. It loads the scene themainScenesetting names inpackage.json.The bug
The owner had
assets/weapons-lab.scene.gltfopen in a tab. They pressed Play. The main scene ran.Step by step, before this change:
store.activeIdisassets/weapons-lab.scene.gltf.startRunModereadsstore.mainScene, not the active document.store.activateForPlay(mainScene.path), which detaches the lab and attaches the main scene.startGameand the whole run then act on the main scene.There is no error text. The editor does exactly what pass 1a told it to do. The plan said so: "Play from any tab activates the main scene document first". That decision was wrong. A scene on screen is the scene you expect to play.
Measured on a copy of the terminator project, before the change, during the run:
lablists the nodes only the weapons lab has. It is empty. The main scene's nodes are there instead.stopRunModehad the same bug on the error path. It readstore.mainScenetoo, so a failedstartGamewould have restored the wrong document's name and dirty flag.There is a second half to the report. Play from a tab that holds no scene has to switch to the main scene to run it. Stop then left the editor on that scene. The tab the user pressed Play on never came back.
The fix
startRunModerunsstore.activewhen it is aSceneDocument, andstore.mainSceneotherwise.beforeRunrecord names the scene the run borrowed.stopRunModerestores that scene, not the main scene.stopRunModeno longer re-checksbeforeRun, because the top of the function already required it.beforeRunalso recordsstore.activeId, read before the activate that Play does.stopRunModeactivates that tab again once the scene is restored.activateForPlayalready does nothing for a path it cannot find or already shows. So a scene tab stays where it is, and a tab closed meanwhile leaves the scene on screen.DocumentStore.mainScene,DocumentStore.activateForPlayandEditorDocument.dirtysay what the code now does.Nothing else moves. The snapshot already serializes what sits under the model root.
startGameruns the project's scripts on the viewer it is given. The tab strip stays disabled while a game runs, by decision 5.Two checks the brief asked for:
startGamenever reads themainScenesetting.RuntimeProject.mainSceneis declared atpackages/engine/src/runtime/createGame.ts:48and read once, at:132, insidecreateGame, which needs it to load its own scene. The editor still fills the field inruntimeProject()(ViewerInstanceManager.ts:844) because the two runtimes share the type. The othermainScenehits in the engine are a local helper insceneSerialization.tsabout a glTF file's default scene, and the validator inprojectFormat.ts:113.main.jsreceives{viewer}alone (createGame.ts:98), so it cannot read the setting. It can read the tree. The terminator project's ownmain.jsalready does: it picksgetComponentOfType('WeaponsLab')orgetComponentOfType('GameManager'). During my runs the page reportedWeaponsLabwhile the lab ran andGameManagerwhile the main scene ran. No runtime change was needed.The risk trade
The cost: Play is no longer one fixed target. A user on a half-built scene tab now runs that scene. A project whose scripts assume the main scene will see another one. That is the owner's own request, and the scene on screen is the honest answer to "what does Play play".
The tab restore costs one more switch at Stop. A switch detaches one tree and attaches another, which is the same work a tab click does every day.
What lowers the cost: the rule is one line with two outcomes, and both are visible in the editor. The running scene is the tab that stays selected. The strip is dead during the run, so the run cannot drift onto another document.
The rejected alternative: a new
store.playScenegetter that both Play and Stop would call. It reads well, but it has one real caller. Stop must restore the scene that ran. It must not re-derive it. ThebeforeRunrecord already names that scene, so the getter would have added a second source of the same truth.Tests
Manual, headless, on my own copy of the terminator project at
/Users/minjunes/games/terminator-play. Served withCI=1 node /Users/minjunes/kite3d-worktrees/play-active/packages/kite3d/dist/cli.js dev --port 4473 --no-open. Playwright with Chrome for Testing 1243, 1512x982, the swiftshader flags. Nothing took focus.The served editor was my build. I put a marker file in
packages/editor/distand it came back over HTTP. The first pass ran againstassets/index-DDLhxgcd.js. The tab restore, re-run after the rebase, ran againstassets/index-XtN193AA.js. The main checkout servesindex-BsvpCduU.js, so neither hash can come from it.Before the change, Play from the weapons lab tab ran the Terminator main menu:
/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/before-03-lab-tab-running.pngAfter the change, the same press ran the weapons lab: the firing range, the turntable, the target dummies, the revolver in hand and the lab HUD:
/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/after-03-lab-tab-running.pngStop put the lab back, pixel for pixel against the shot taken before the run:
/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/after-02-lab-tab-edit.png/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/after-05-after-stop.pngThe main scene still runs from the main scene tab and from a non-scene tab:
/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/after-07-main-tab-running.png/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/after-10-object-tab-running.png/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/after-11b-object-tab-reclicked.png/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/after-16-texture-tab-running.pngStop gives the tab back. I pressed Play on four tabs in one session, re-run after the rebase. I read the active id and the tree at each step.
The shots for those steps:
/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/rebased-20-object-tab-edit.png/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/rebased-21-object-tab-running.png/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/rebased-22-object-tab-after-stop.png/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/rebased-25-lab-tab-after-stop.png/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/rebased-28-main-tab-after-stop.png/private/tmp/claude-501/-Users-minjunes-blitz/278f5f97-7e16-4722-b1bc-c061014966db/scratchpad/evidence/play-active/shots/rebased-31-texture-tab-after-stop.pngThe readouts, step by step, are in
before-report.json,after-report.json,after-edges.json,texture-tab.json,after-fix-report.jsonandrebased-report.jsonin the same folder. Zero page errors in every run.The name and the saved hash prove the restore. The weapons lab document carried
sceneName"Weapons Lab" and hashc0fcf1b165. During the run those read "Scene" and73274aff1f, the snapshot's own. After Stop they were "Weapons Lab" andc0fcf1b165again.The edges:
Turntablefrom x -16 to -9 without saving. During the run the lab ran. After Stop the node was still at -9.aria-disabled="true"and lost its close cross.startGamethrows, and its catch callsstopRunMode. That now restores the scene that ran. Before the change it restored the main scene. That is the wrong document when a second scene plays.Off the happy path, one thing stays as it was:
mainScenesetting still refuses Play from a non-scene tab, exactly as before.One thing to know about the dirty flag. In the first session both scene documents in that project copy read dirty at rest, before and after the change. That is pre-existing noise in the copy. The tab restore session opened the weapons lab clean. It ran, and Stop left it clean, with its name and hash back. So the flag round trips in both states.
Suites, all run in the worktree after the change:
npm run typecheck: three workspaces, clean.npm run lint: three workspaces, clean.npm test -w packages/kite3d: 10 tests in 5 files, all pass, 6.88s.npm run test:scripts: 2 tests, both pass.Two tests are new, in
packages/kite3d/test/editor-open-tab.test.ts. They sit beside the headless test already there. The owner's two reports justify them. They take 1.9s and 2.4s.The first builds a temporary project with two scenes. It opens the second one with the Files panel double click, the way a user does. It presses the navbar Run button. It then reads the model root. It asserts the second scene's node is there and the main scene's node is not.
The second opens
pixel.png, a texture tab, and presses Run. It asserts the main scene runs and its tab is selected. It presses Edit. It then asserts the texture tab is selected again and the main scene's node is gone. I ran it three times in a row. It passed each time, in 2.3s, 2.5s and 2.4s.What fails without the fix, measured twice. I reverted the scene choice to
store.mainScene, rebuilt the editor and ran the suite. The first new test failed withexpected [ 'Scene', 'MainSceneTriangle' ] to include 'SecondSceneTriangle'. I then deleted the line that activatesbefore.activeIdand rebuilt. The second new test failed, waiting for thepixel.pngtab to come back. I restored both lines and rebuilt each time.Anti-slop deletions in my own code, by the S1 to S10 lens:
if (this.beforeRun)block at the end ofstopRunModeis gone. The function already requires the record at the top.stopRunModeno longer re-derives the scene fromstore.mainScene. ThebeforeRunrecord is the one place that names the scene the run borrowed.sceneGltfhelper has two callers in that file.rgchecks for the names I removed, all zero hits outsidedistandnode_modules: "Play runs the main scene", "Play borrows the main scene", "activates the main scene", "whatever tab was showing".store.mainScenenow has one reader, the fallback instartRunMode.The five review questions:
beforeRunholds aSceneDocument, andstore.active instanceof SceneDocumentis the narrowing that makes the choice safe.beforeRunwas already the record of the run; it now names the scene as well, and its comment says so.Not tested: a material tab. This project has no
.matfile. It takes the same branch as the object and texture tabs. Both of those ran the main scene. The code tests no file kind beyondinstanceof SceneDocument.Deploy
The editor bundle ships this, through the next
kite3dnpm release. Nothing else changes.Rollback: publish or pin the previous version on npm. A project pins
kite3din itsdevDependencies, sonpm i -D kite3d@0.21.0-alpha.5puts the old Play back.