32
Scene editing
MelekTaus edited this page 2026-10-07 23:25:25 +02:00

Placing an asset: drag it in. Drag a row out of the Assets panel and drop it on the view — the Scene tab places into the active scene, the Container tab into the open template, so what you see is what you fill. While the pointer is over a view, an outline shows the footprint the object will have and a crosshair the origin it snaps to; it turns red when the drop would be refused, which today means a container that would end up inside itself (placementRejection, src/core/model/placement.ts — the same function the command runs, so the preview cannot promise a placement the drop then denies). Picking a container up selects it, but the Container tab does not follow that while a drag is in flight: the target must not swap out from under the pointer. Dragging is the only way to place: a double-click on an asset opens its tab (a scene also becomes the active one), it never places.

Zoom in whole steps. The wheel in the Scene and Container tabs and in the animation preview moves the zoom one step at a time: 1/8, 1/4, 1/2, then 1 to 6, 8, 12, 16, 24, 32, 48 and 64 screen pixels per world pixel. A mouse notch is one step, a trackpad takes one every 50 pixels of scrolling. Fit rounds down to the step that still fits. The steps count device pixels, so zoom 1 stays one screen pixel per world pixel even with Windows display scaling at 125% or 150%. With every texel getting the same number of screen pixels, pixel art never shows one pixel wider than its neighbour, wherever the view is panned. Sprites that are turned or scaled by an odd factor still cannot be even.

Fit in the Scene tab frames what the game shows. The Scene tab has a header bar like every other tab, with Fit and the eye. With a camera switched on, Fit shows the world exactly as the Game tab does at the scale picked there: the same zoom and the camera's centre on the same screen pixel, so switching between the two tabs moves nothing (freeViewOfCamera, src/viewport/view.ts). At Auto that is a whole step, at Fit the fraction the Game tab came to. The camera is the one the Game tab previews when nothing is playing. The two views match only while they are the same size: with the tilemap panel open beside the scene view, the view is narrower and Auto may come to a smaller step. Without a camera, Fit frames the whole content in a whole step as before.

A scene comes up framed. When a project is loaded and when another scene is opened, the Scene tab shows it as if Fit had been clicked (#295). From then on the view is yours: nothing moves it until the next scene is opened. A scene with nothing in it stands at the origin, one screen pixel per world pixel. A scene the running game loads is not framed anew, and the Container tab keeps the view of each container as before.

Editor symbols are always on top. The viewport draws scene content (sprites) in an objects layer and editor symbols (camera icon, labels) in an overlay layer above it, so a sprite can never hide a symbol regardless of z or hierarchy order. A viewport label keeps a constant size on screen however far you zoom, and both kinds of them — the object names of the Scene tab and the frame names of the Texture tab — read that size from one token (Theme.viewportLabelSize); the two viewportLabel styles differ in colour only.

The symbol icons are the asset panel's icons. Behavior, container, rigidbody, collider, camera and particle source each show the same file in the Scene tab as in the Assets panel (src/editor/panels/icons/, drawn by src/viewport/symbolIcons.ts): white, in a dark outline, with no plate behind it, so every symbol reads the same on any ground. The outline is not a second file: haloSvg (src/ui/svg.ts) widens every shape of the icon by its stroke, and that silhouette is drawn dark under the icon. An icon is 18 world units across times the project's Symbol scale (menu → Project settings…, 0.1 to 4, default 1), stays upright and grows and shrinks with the zoom like the scene. The scale also sets the spacing of icons that share a spot and the outline of a selected behavior, container or rigidbody; a project of 16-pixel art may want 0.5. A file draws in its own units with the origin at its centre, and currentColor stands for the colour it is drawn in. Pixi's SVG parser is not a browser, so the files keep to what both draw alike, and tests/ui/svg-icons.test.ts holds them to it: only path, circle, rect, ellipse and line (Pixi reads polygon points as whole numbers), no transform (ignored), every shape states its fill (a browser fills a shape without one black, Pixi does not), colours as #rrggbb, none or currentColor. What stays code is what depends on the object: the camera's frame and a collider's outline.

Icons make room for each other. Every icon wants the position of its object. Icons that would overlap there, whoever owns them and whether they share a parent or not, share the spot instead: they stand on a ring round the middle of their positions, evenly spaced and one icon apart, and the ring starts on the left. Two icons on one spot, such as a crate's collider and rigidbody, stand left and right of it, three form a triangle, and so on. A ring that would reach into another icon takes that one in as well, until nothing overlaps (layoutSymbols, src/viewport/symbolLayout.ts). Switched-off objects take no room. A name stands beside its icon, on the outer side of a ring. Every icon is clicked like its object: a click selects it and a drag moves it, even where the icon lies over a sprite, and the selection outline of a behavior, container or rigidbody is drawn round its icon. Beside the icons the sprite underneath is picked as before.

Names stand on plates, in rows of their own. A symbol's name is drawn in the light label colour on a dark, 70 % opaque plate, so it reads on any ground, and a selected object's plate gets an accent frame. A name that would cover another name or an icon moves a row down, up, two down and so on until the row is free (stackLabels, src/viewport/labelRows.ts). Names are placed top to bottom, so the upper of two keeps its row, and a name only moves up or down: it stays on its icon's side. Names off screen take their row as well, so none jumps while the view pans.

The eye picks what the Scene tab shows. Beside Fit in the tab's header, the eye opens a list of every type that can be placed, Behavior to Tilemap, and per type up to three switches: Symbol (its icon), Name (its name plate) and Shape: on a collider its outline, on a tilemap the outline of its solid cells, while the map's tiles stay drawn. The Camera row has a Shader switch instead, off by default: it lays the main camera's shader over its frame (#299, see Shaders). Sprite, Text and Tilemap have no icon, so their Symbol switch is greyed out; their Name switch puts a plate centred on the object, and it starts off. Every other switch but the Shader starts on, which is what the Scene tab always showed. A hidden icon takes no room on a ring and draws no owner link, and its name then stands centred on the object. A hidden icon or outline cannot be clicked either: the object is still selected in the hierarchy. The list stays open while you switch. The switches are saved with the project (sceneVisibility, src/core/model/sceneVisibility.ts), so whoever opens it sees the same, but like the game scale they are no undo step: they change how you look, not the project. The Game tab and the running game are not affected.

Z-order is part of the position. position.z is the draw order of a scene object: higher z is drawn on top; objects with equal z keep their hierarchy (tree) order. It is edited in the inspector next to X/Y, undoable like any move, and applies in edit and play mode alike. In a running game a graph reads it with Get Z and changes it with Set Z (see Behaviors). The layers of a tilemap are sorted in one by one, each at the map's z plus its own (see Tilemaps, #174). On the z of a layer sorted by Y, Y decides instead: whatever stands higher up is drawn first (see Tilemaps, #228).

Sort with parent. A child's z adds up with its parent's, and on its own the child is sorted against the whole scene: at z -1 under a figure on 0 it would fall behind everything on 0. Sprites, texts and particle sources therefore have the switch Sort with parent, right under Z in the inspector (#232). With it on, the child is drawn in its parent's place: against the rest of the scene only the parent counts, with its z and, on a z sorted by Y, its feet. The child's own z then only orders it against the parent and their siblings: below 0 behind the parent, 0 and above in front of it, ties in hierarchy order. It chains, so a grandchild with the switch on sorts with the topmost object of the chain. A new asset starts with the switch on; an asset stored before it had it reads as off, so older projects draw as they did. It is an ordinary property: set on the asset, overridden per object. A parent that is not drawn itself (container, behavior, camera, collider) or is a tilemap has no single place to share, so there the switch does nothing. The rule lives in drawSlots (src/viewport/drawOrder.ts), which the scene view, the game and picking share.

A container has the switch too (#243), but it works differently: the container is never drawn, so it passes its content on. With it on, everything the container holds is drawn in the place of the container's parent, say the figure it hangs under, as if it were the figure's own children. The switch of the content itself has no say then. Several containers in a row pass on through as long as each has it on. A container starts with the switch off, also a new one, so content placed before draws as it did. At the top level, or under a tilemap, it does nothing.

Transform. Sprites, texts, colliders and containers carry four more rows next to their position: Scale (one factor, default 1), Rotation (degrees, positive turns clockwise on screen), Mirror X and Mirror Y. They are ordinary properties — set on the asset, overridden per object, gold when they deviate, writable from a graph through Properties — and they cascade like a position: a child is placed inside its parent's turn and size, so a whole group turns around its parent's origin (src/core/properties/transform.ts composes the chain as matrices; src/core/model/matrix.ts is the arithmetic). Cameras and behaviors have no rows of their own — they are placed by the chain but never turned or sized (the camera's view stays upright) — and the editor's icons stay upright too. The selection outline follows the corners; dragging an object moves it in its parent's frame, so a child under a parent turned by a quarter walks down the screen when dragged along its own x. Move and Set position work in the object's own axes as well.

Size gizmo. A selected sprite or collider gets handles on its outline (#223), turned with it. Texts, tilemaps, particle sources and containers get the knob only (#242), since they have no width and height to pull at:

  • Edges and corners set Width and Height (a corner both). The opposite side stays where it was, so the object's position moves by half the change. Sizes land on whole units, at least 1. A handle reaches 7 pixels outwards but only half that into the object, so a small sprite can still be grabbed and moved close to its edge.
  • A circle collider has edge handles only: any edge sets the radius, and the circle stays round. A polygon or frame collider has no size handles, its corners are edited on the outline itself.
  • The knob on a stem above the top edge turns or scales the object. The first few pixels of the drag decide which: pulled away from or towards the centre it scales, pulled sideways it turns. That choice holds until the release, so one drag never does both. Shift snaps a turn to 15°. The turn is the angle swept round the object's centre, so it can go on past a half circle.
  • A container's knob stands over a thin frame round everything it holds, not over its small icon. The frame turns and grows with the container. Turning and scaling go round the container's origin, where its icon stands, so its content swings round that point.
  • The values go into this object's overrides, like "Fit to sprite": a value equal to the asset's own clears the override. One drag is one undo step.

The rules are in src/editor/sizeGizmo.ts, the handles in src/editor/viewport/sizeGizmoEditor.ts.

Hierarchy (parenting). Every scene object may have a parent in the same scene (SceneObject.parentId). Positions — including z — are local to the parent; the world position is the parent's transform applied to the local one, all the way up (z simply adds). Children move with their parent and are removed with it (also when the parent's asset is deleted); undo brings the whole subtree back. Re-parenting keeps the world position and look: the local position is recomputed, and when the new parent chain is turned or sized differently the object's transform rows absorb the difference (between two upright parents nothing is written). Cycles are rejected. In the Scene objects panel children are indented below their parent; drag a row onto another row to parent it, onto the empty space below the rows (or right-click → Unparent) to move it to the top level; the inspector offers the same as a Parent menu. Dragging into the gap above or below a row — its outer quarter, marked with a line instead of an outline — orders the object among that row's siblings rather than putting it inside, and re-parents it on the way when the gap belongs to another parent. Since equal z falls back to this order, that is also how two objects at the same depth are put in front of each other. A drag that lands where it started changes nothing and costs no undo step. A row's name turns gold when the object deviates from what it follows — its container template, or its asset — and also when anything below it does, so a folded subtree still shows that something inside was changed. It is the same gold the inspector paints an overridden label in. A click on a row selects the object; a click on the row that is selected already pings its asset in the asset panel, as a click on the asset link in the inspector does, and the selection stays. A double-click on a row (or Enter) opens the object's asset, selected or not, as a double-click on that link does (#353). Rows with children fold: click the chevron (or press ←/→) to open and close a subtree; a group's row also folds on a double-click. Right-click → Expand all / Collapse all for the whole scene. A scene opens fully collapsed — which rows are open is view state of the panel, never stored in the project — and selecting an object anywhere (viewport, Add collider…, Create container) unfolds the way down to it and scrolls it into view, so a selection is never hidden. In play mode entities carry the same parent link: moving a parent entity moves its subtree, Destroy removes it. Get position gives the coordinates local to the parent, Get world position the world ones.

Groups in the hierarchy (#241). A group is a folder for top-level objects — 100 crates under one foldable row, say. It is editor looks only: parents, positions and behaviors stay exactly as they are, and the game never sees it. Select one or more top-level objects, right-click → Group…, name it; the row shows the name, the number of members and a folder in the icon column of the objects, the members indented below it and their own children one step further in. The dialog's Group dropdown also lists the existing groups: pick one and Add puts the objects into it instead of making a new group. Only top-level objects go in: the entry is greyed out as soon as a child is selected, and an object put under a parent leaves its group. There are no groups inside groups.

  • Clicking the group row selects all members, so they move together in the view; Ctrl-click adds or takes them off the selection. A group whose members are all selected stays folded; one selected member alone unfolds it, like a parent.
  • A plain click on a row that is already selected along with others keeps only that row selected, a member of a group included. The press alone changes nothing yet, the release does, so a drag that starts there leaves the selection as it was.
  • Shift-click selects a range: every row from the last plain or Ctrl-click to the clicked one, upwards or downwards. A folded group in the range brings all its members; children folded away under a parent stay out. A second Shift-click redraws the range from the same row, Ctrl+Shift adds the range to the selection. Without a selected row to start from, Shift-click adds the one row as before.
  • Dragging an object onto the group row puts it in behind the last member; dropping it into the gap between two members puts it there, dropping it beside a row outside the group takes it out. The group row itself drags as one block between top-level rows. Dragging a row that is part of a multi-selection moves all selected objects, side by side in their listed order and as one undo step; a selected child of a selected object travels with its parent.
  • The group row's menu has Select members, Rename… and Ungroup (the members stay).
  • Spawned objects are grouped by asset (#265). An object a Spawn node places at the top level goes into the group named after its asset, Gold for the gold pickups; a group of that name that is already there is used, otherwise one is made. Two hundred pickups are then one folded row. A spawned object with a parent stays under its parent. The name of the object does not change, so a Find looks for it as before; only objects of the same group are drawn as one block at equal z. The object menu adds Remove from group. Grouping, renaming, ungrouping and moving in or out are one undo step each.
  • Order: a group is listed where its first member stands, with the others moved up behind it. That listed order is also the order equal z is drawn in (groupedOrder in src/core/model/hierarchy.ts), in the editor and in the game, so the list never shows a different order than the one drawn. Grouping objects can therefore change which of two objects at the same z is in front.
  • Which groups are open is view state like a folded parent: a reloaded scene starts folded.
  • A copy pasted at the top level of the same scene lands in its original's group. A container made from members of one group takes their place in it; its template has no groups of its own. A container tab can group its template's top level the same way, and a placed container shows its content in that order.
  • Stored as groups: [{ id, name }] on the scene or container and groupId on the object; a group left without members disappears from the list and is dropped on the next load.

The object menu in the view. Right-clicking an object in the scene view opens the same menu its row in Scene objects has (#204) — one function builds both (src/editor/objectMenu.ts), so they cannot drift apart. Only Expand all / Collapse all stay in the panel, where they mean something. Which object the menu is for follows the rule of a press: a selected object under the pointer, hidden behind others or not, keeps the selection, and the menu acts on all of it; otherwise the topmost object there is selected first. Empty space, an empty tilemap cell included, opens a menu with Paste alone. The Container tab does the same for its template.

Copy and paste (#240). Ctrl+C copies the selected objects, each with everything below it (children, colliders, rigidbodies), and Ctrl+V pastes them into the scene or container the visible tab shows. The object menu offers the same as Copy and Paste. The copy keeps names, overrides and the look (rotation, scale, mirror); the editor offset stays behind. A placed container is copied as a placed container and keeps showing its template, content picked out of one is pasted as ordinary objects. The clipboard is the editor's own and holds ids of this project, so it lives until another project is opened.

  • Where it lands: under the pointer when the pointer is over the view (the copied group keeps its layout, centred there), else one grid step (100) down and right of the original, and another step for every earlier paste that is still there. Both go by what the view shows: the editor offsets of the original and of the new parents count (#263). The copy itself has no offset, so in the game it stands where the view drew it, less the offsets of its parents.
  • Into which parent: into the one selected object (from the menu: the one clicked). When that object is an original or an earlier paste, the copy goes beside it instead, under its parent, so pasting again and again never stacks copies inside each other. Without such an object the copy keeps the parent it had, if this scene or container has it, else it goes to the top level. It keeps its look under the new parent, as a re-parent does.
  • Refused: a container pasted into its own template (or into anything it ends up inside), with the notice a drop gets. One paste is one undo step and selects what it pasted.
  • While objects are copied, Ctrl+V in the Scene and Container tab pastes them; an image from the system clipboard is then pasted from another tab. With nodes copied it goes through there.

Editor offset (scene view only). Every scene object additionally carries an editorOffset — an x/y shift of where it is drawn, selected and dragged in the Scene tab, and nothing else. The game, collisions and the graph never see it; it is there to pull apart objects that sit on top of each other, such as the stacked title/win/lose screens of a game. It cascades like a position (offsetting a container offsets its content) and is edited in the inspector below X/Y/Z, undoable, with ↺ back to 0. It belongs to the placed object, so Create container and placing a container never carry it into the template.