Pick an existing collider or rigidbody from the object menu (#222)
"Add collider" and "Add rigidbody" could only create a fresh asset.
Reusing one meant creating a copy by hand, and a new box collider
never matched the sprite it was added to.
- "Add collider…" and "Add rigidbody…" open a second menu at the same
spot: a search field, "Create new …", a separator, then every asset
of that kind sorted by name. Enter takes the first match.
- An existing asset is placed as a shared child, not copied. An asset
the object already carries is greyed out.
- The rule for when the entries are available lives in one place in
the collider commands. An object without a resolvable asset is
greyed out again, as with the old canAddCollider.
- "Fit to sprite" in the menu of a placed collider sizes it to its
sprite and centres it (position 0,0, z kept) as one undo step.
- Fit writes overrides of that placement, not the asset, because a
collider can now sit on several sprites. A value equal to the asset
value clears the override instead.
- Fit removes the collider's own scale, swaps width and height near
90 degrees and gives a circle the radius that fits (min/2). It is
greyed out for polygon and frame outlines and off a sprite.
- Menu has no submenus, so a second menu opens instead, similar to
"Add Component" in Unity. That costs one click more than before.
Tests:
- ./check.sh: 152 files, 2234 tests, CHECK EXIT 0 (before 151/2228).
- 6 new unit tests in tests/core/collider-parts.test.ts.
- 3 new e2e checks in 08a: submenu with search and greying, an
existing collider via search and Enter, Fit on oc2.
- 09, 40 and 41 follow the new labels.
- e2e 1-16,39-41 on the final build: ALL PASSED, 596 checks.
- Counter-checks: no greying, existing asset ignored and fit without
centring each fail at their own check. Breaking the override rule
fails 2 unit tests.
- CI confirmed 1345 checks at 2cf7202 (task 265). New total 1348,
calculated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2cf72022eb
commit
abf3b1f618
9 files changed
+361
-72
No files matched your search
+18
-16
@@ -4,14 +4,19 @@
|
||||
*/
|
||||
import { annotationNoun, RemoveAnnotationCommand } from '../core/commands/annotationCommands';
|
||||
import { DeleteAssetCommand, SetActiveSceneCommand } from '../core/commands/assetCommands';
|
||||
import { AddColliderCommand, AddRigidbodyCommand, rigidbodyRefusal } from '../core/commands/colliderCommands';
|
||||
import {
|
||||
AddColliderCommand,
|
||||
AddRigidbodyCommand,
|
||||
colliderRefusal,
|
||||
rigidbodyRefusal,
|
||||
} from '../core/commands/colliderCommands';
|
||||
import { oneStep } from '../core/commands/CompoundCommand';
|
||||
import { CreateContainerCommand } from '../core/commands/containerCommands';
|
||||
import { RemoveNodeCommand } from '../core/commands/graphCommands';
|
||||
import { RemoveSceneObjectsCommand } from '../core/commands/sceneObjectCommands';
|
||||
import type { AssetType } from '../core/model/AssetTypes';
|
||||
import { generatesPictures } from '../core/model/generated';
|
||||
import { isLiveInstance, isTemplateContent } from '../core/model/instance';
|
||||
import { isTemplateContent } from '../core/model/instance';
|
||||
import type { Command } from '../core/commands/Command';
|
||||
import { findAnnotation } from '../core/graph';
|
||||
import { findAsset, findGraphAsset, type ObjectHolder, type Project } from '../core/model/Project';
|
||||
@@ -39,20 +44,17 @@ export function createContainer(state: EditorState, holderId: string, objectIds:
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The test `AddColliderCommand` makes, so no place offers what it would refuse. */
|
||||
/** The test `AddColliderCommand` makes; template content has no structure of its own to change. */
|
||||
export function canAddCollider(project: Project, holder: ObjectHolder, object: SceneObject): boolean {
|
||||
const type = findAsset(project, object.assetId)?.type;
|
||||
return (
|
||||
type !== undefined &&
|
||||
type !== 'collider' &&
|
||||
!isTemplateContent(holder, object.id) &&
|
||||
!isLiveInstance(project, holder, object)
|
||||
);
|
||||
return !isTemplateContent(holder, object.id) && colliderRefusal(project, holder, object) === null;
|
||||
}
|
||||
|
||||
/** Gives the object a collider child sized like it; selects the collider. */
|
||||
export function addCollider(state: EditorState, holderId: string, objectId: string): boolean {
|
||||
const command = new AddColliderCommand(holderId, objectId);
|
||||
/**
|
||||
* Gives the object a collider child – a new one sized like it, or the existing `assetId` (#222);
|
||||
* selects the collider.
|
||||
*/
|
||||
export function addCollider(state: EditorState, holderId: string, objectId: string, assetId = ''): boolean {
|
||||
const command = new AddColliderCommand(holderId, objectId, assetId);
|
||||
if (!state.execute(command)) return false;
|
||||
state.select(objectSelection(holderId, [command.objectId]));
|
||||
return true;
|
||||
@@ -63,9 +65,9 @@ export function canAddRigidbody(project: Project, holder: ObjectHolder, object:
|
||||
return !isTemplateContent(holder, object.id) && rigidbodyRefusal(project, holder, object) === null;
|
||||
}
|
||||
|
||||
/** Gives the object a rigidbody child; selects the rigidbody. */
|
||||
export function addRigidbody(state: EditorState, holderId: string, objectId: string): boolean {
|
||||
const command = new AddRigidbodyCommand(holderId, objectId);
|
||||
/** Gives the object a rigidbody child, new or the existing `assetId`; selects the rigidbody. */
|
||||
export function addRigidbody(state: EditorState, holderId: string, objectId: string, assetId = ''): boolean {
|
||||
const command = new AddRigidbodyCommand(holderId, objectId, assetId);
|
||||
if (!state.execute(command)) return false;
|
||||
state.select(objectSelection(holderId, [command.objectId]));
|
||||
return true;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { colliderFit, FitColliderCommand } from '../core/commands/colliderCommands';
|
||||
import { UnpackContainerCommand } from '../core/commands/containerCommands';
|
||||
import { SetParentCommand } from '../core/commands/sceneObjectCommands';
|
||||
import { findObject } from '../core/model/hierarchy';
|
||||
@@ -64,15 +65,24 @@ export function openObjectMenu(
|
||||
)
|
||||
: []),
|
||||
{
|
||||
label: 'Add collider',
|
||||
label: 'Add collider…',
|
||||
disabled: !object || many || !canAddCollider(state.project, source, object),
|
||||
onSelect: () => addCollider(state, holder.id, id),
|
||||
onSelect: () => openPartMenu(state, 'collider', holder, id, x, y),
|
||||
},
|
||||
{
|
||||
label: 'Add rigidbody',
|
||||
label: 'Add rigidbody…',
|
||||
disabled: !object || many || !canAddRigidbody(state.project, source, object),
|
||||
onSelect: () => addRigidbody(state, holder.id, id),
|
||||
onSelect: () => openPartMenu(state, 'rigidbody', holder, id, x, y),
|
||||
},
|
||||
...(object && asset?.type === 'collider'
|
||||
? [
|
||||
{
|
||||
label: 'Fit to sprite',
|
||||
disabled: many || typeof colliderFit(state.project, source, object) === 'string',
|
||||
onSelect: () => state.execute(new FitColliderCommand(holder.id, id)),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: many ? `Create container from ${selected.length} objects` : 'Create container',
|
||||
disabled: selected.some((oid) => isTemplateContent(source, oid)),
|
||||
@@ -102,6 +112,39 @@ export function openObjectMenu(
|
||||
return menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which collider or rigidbody to add (#222): a new one, or any the project has, found by typing. One
|
||||
* the object wears already is greyed out – a second copy of it would only double up.
|
||||
*/
|
||||
function openPartMenu(
|
||||
state: EditorState,
|
||||
type: 'collider' | 'rigidbody',
|
||||
holder: ObjectHolder,
|
||||
id: string,
|
||||
x: number,
|
||||
y: number,
|
||||
): Menu {
|
||||
const add = type === 'collider' ? addCollider : addRigidbody;
|
||||
const worn = new Set(holder.objects.filter((o) => o.parentId === id).map((o) => o.assetId));
|
||||
const existing = state.project.assets
|
||||
.filter((a) => a.type === type)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const menu = new Menu(
|
||||
[
|
||||
{ label: `Create new ${type}`, onSelect: () => add(state, holder.id, id) },
|
||||
{ separator: true },
|
||||
...existing.map((a) => ({
|
||||
label: a.name,
|
||||
disabled: worn.has(a.id),
|
||||
onSelect: () => add(state, holder.id, id, a.id),
|
||||
})),
|
||||
],
|
||||
{ searchable: true },
|
||||
);
|
||||
menu.open(x, y);
|
||||
return menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Open texture", "Open shader", … – one entry per asset the values point at (#221), skipping kinds
|
||||
* that have no tab to open. The asset panel offers the same for an asset's own values.
|
||||
|
||||
Reference in new issue
Block a user