Place assets by dragging them from the panel into the view

The context menu entry is gone; dragging replaces it entirely. The drop
target is the view under the pointer, not the active tab, so the same
gesture works for scenes and for container templates.

While dragging, an outline shows the footprint the object will have plus
a crosshair on its origin, turning red when the drop would be refused.
Preview and command share one function (placementRejection), so the
outline cannot promise something the drop then rejects.

A left-press used to select the list row immediately. Since the container
tab follows the selection, it switched to the very container being dragged
and every drop would have nested it inside itself. The list now selects on
release without a drag: picking something up is not the same as choosing it.
This commit is contained in:
root committed 2026-09-09 13:09:13 +02:00
1 parent 14562cf157
commit 75d377af39
18 files changed
+545 -77

No files matched your search

+21 -9
View File
@@ -1,11 +1,17 @@
import { CreateAssetCommand, SetActiveSceneCommand } from '../../core/commands/assetCommands';
import { nextAssetName } from '../../core/commands/naming';
import { PlaceAssetCommand } from '../../core/commands/sceneObjectCommands';
import { ASSET_TYPE_LABELS, ASSET_TYPES, type AssetType, hasColorSwatch } from '../../core/model/AssetTypes';
import {
ASSET_TYPE_LABELS,
ASSET_TYPES,
type AssetType,
hasColorSwatch,
isPlaceableType,
} from '../../core/model/AssetTypes';
import type { Vec2 } from '../../core/model/Properties';
import { findAsset, type ObjectHolder } from '../../core/model/Project';
import type { TextureStore } from '../../core/store/TextureStore';
import { Button, ListView, Menu, type MenuItem, Panel, ScrollContainer } from '../../ui';
import { Button, ListView, Menu, type MenuItem, Panel, ScrollContainer, startDrag } from '../../ui';
import { confirmDeleteAsset } from '../actions';
import { type EditorState, objectSelection } from '../state';
import { pickAndImport } from '../textureImport';
@@ -47,6 +53,7 @@ export class AssetPanel extends Panel {
this.list.onSelect = (id) => state.select(id ? { kind: 'asset', assetId: id } : null);
this.list.onActivate = (id) => this.activate(id);
this.list.onContextMenu = (id, x, y) => this.openContextMenu(id, x, y);
this.list.onDragOut = (id) => this.startPlacementDrag(id);
this.scroll.setContent(this.list);
this.content.add(this.scroll, 1);
@@ -100,6 +107,18 @@ export class AssetPanel extends Panel {
else this.place(id);
}
/** Where an asset's row sits on screen, so a check can pick it up by id (test hook). */
rowGlobal(assetId: string): { x: number; y: number } | null {
return this.list.rowGlobal(assetId);
}
/** Hands the asset to the viewport under the pointer; a scene has nowhere to be placed into. */
private startPlacementDrag(assetId: string): void {
const asset = findAsset(this.state.project, assetId);
if (!asset || !isPlaceableType(asset.type)) return;
startDrag({ kind: 'asset', id: assetId }, asset.name);
}
private place(assetId: string): void {
const target = this.target();
if (!target) {
@@ -121,13 +140,6 @@ export class AssetPanel extends Panel {
onSelect: () => this.state.execute(new SetActiveSceneCommand(id)),
disabled: this.state.project.activeSceneId === id,
});
} else {
const target = this.target();
items.push({
label: target?.holder.type === 'container' ? 'Place in container' : 'Place in scene',
onSelect: () => this.place(id),
disabled: !target,
});
}
items.push({ label: 'Delete…', danger: true, onSelect: () => confirmDeleteAsset(this.state, id) });
const menu = new Menu(items);