Compare commits

...

4 Commits

Author SHA1 Message Date
Florian Rival
825aaebe22 Fix flow 2025-06-23 18:49:51 +02:00
Florian Rival
f63a823fd0 Improve angle related descriptions and wiki links 2025-06-23 18:43:40 +02:00
Florian Rival
f7f63847c5 Add hint to scrap old instances descriptions for AI agent 2025-06-23 00:55:50 +02:00
Florian Rival
deaa49d2bf [WIP] Improve AI instances painting 2025-06-23 00:19:01 +02:00
8 changed files with 666 additions and 73 deletions

View File

@@ -235,7 +235,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddAction("SetAngle",
_("Angle"),
_("Change the angle of rotation of an object (in degrees)."),
_("Change the angle of rotation of an object (in degrees). For "
"3D objects, this is the rotation around the Z axis."),
_("the angle"),
_("Angle"),
"res/actions/direction24_black.png",
@@ -250,7 +251,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddAction("Rotate",
_("Rotate"),
_("Rotate an object, clockwise if the speed is positive, "
"counterclockwise otherwise."),
"counterclockwise otherwise. For 3D objects, this is the "
"rotation around the Z axis."),
_("Rotate _PARAM0_ at speed _PARAM1_ deg/second"),
_("Angle"),
"res/actions/rotate24_black.png",
@@ -634,7 +636,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddCondition("Angle",
_("Angle"),
_("Compare the angle of the specified object."),
_("Compare the angle, in degrees, of the specified object. "
"For 3D objects, this is the angle around the Z axis."),
_("the angle (in degrees)"),
_("Angle"),
"res/conditions/direction24_black.png",
@@ -1268,7 +1271,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
obj.AddExpression("Angle",
_("Angle"),
_("Current angle, in degrees, of the object"),
_("Current angle, in degrees, of the object. For 3D "
"objects, this is the angle around the Z axis."),
_("Angle"),
"res/actions/direction_black.png")
.AddParameter("object", _("Object"));

View File

@@ -365,6 +365,8 @@ class GD_CORE_API InitialInstance {
* the same initial instance between serialization.
*/
InitialInstance& ResetPersistentUuid();
const gd::String& GetPersistentUuid() const { return persistentUuid; }
///@}
private:

View File

@@ -1405,6 +1405,7 @@ interface InitialInstance {
double GetCustomDepth();
[Ref] InitialInstance ResetPersistentUuid();
[Const, Ref] DOMString GetPersistentUuid();
void UpdateCustomProperty(
[Const] DOMString name,

View File

@@ -1167,6 +1167,7 @@ export class InitialInstance extends EmscriptenObject {
setCustomDepth(depth: number): void;
getCustomDepth(): number;
resetPersistentUuid(): InitialInstance;
getPersistentUuid(): string;
updateCustomProperty(name: string, value: string, globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): void;
getCustomProperties(globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): MapStringPropertyDescriptor;
getRawDoubleProperty(name: string): number;

View File

@@ -44,6 +44,7 @@ declare class gdInitialInstance {
setCustomDepth(depth: number): void;
getCustomDepth(): number;
resetPersistentUuid(): gdInitialInstance;
getPersistentUuid(): string;
updateCustomProperty(name: string, value: string, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): void;
getCustomProperties(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): gdMapStringPropertyDescriptor;
getRawDoubleProperty(name: string): number;

View File

@@ -108,6 +108,7 @@ export const FunctionCallRow = React.memo<Props>(function FunctionCallRow({
details = result.details;
hasDetailsToShow = result.hasDetailsToShow;
} catch (error) {
console.error('Error rendering function call:', error);
text = (
<Trans>
The editor was unable to display the operation ({functionCall.name})

View File

@@ -520,10 +520,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
<Trans>
The AI agent will build simple games or features for you.{' '}
<Link
href={getHelpLink('/interface/ask-ai')}
href={getHelpLink('/interface/ai')}
color="secondary"
onClick={() =>
Window.openExternalURL(getHelpLink('/interface/ask-ai'))
Window.openExternalURL(getHelpLink('/interface/ai'))
}
>
It can inspect your game objects and events.
@@ -535,10 +535,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
<Trans>
The AI chat is experimental and still being improved.{' '}
<Link
href={getHelpLink('/interface/ask-ai')}
href={getHelpLink('/interface/ai')}
color="secondary"
onClick={() =>
Window.openExternalURL(getHelpLink('/interface/ask-ai'))
Window.openExternalURL(getHelpLink('/interface/ai'))
}
>
It has access to your game objects but not events.

View File

@@ -52,6 +52,7 @@ export type EditorFunctionGenericOutput = {|
message?: string,
eventsForSceneNamed?: string,
eventsAsText?: string,
instancesForSceneNamed?: string,
objectName?: string,
behaviorName?: string,
properties?: any,
@@ -154,19 +155,6 @@ const extractRequiredString = (args: any, propertyName: string): string => {
return value;
};
/**
* Helper function to safely extract required number arguments
*/
const extractRequiredNumber = (args: any, propertyName: string): number => {
const value = SafeExtractor.extractNumberProperty(args, propertyName);
if (value === null) {
throw new Error(
`Missing or invalid required number argument: ${propertyName}`
);
}
return value;
};
const makeGenericFailure = (message: string): EditorFunctionGenericOutput => ({
success: false,
message,
@@ -1199,8 +1187,16 @@ const describeInstances: EditorFunction = {
getInstancesInLayoutForLayer(initialInstances, layerName).forEach(
instance => {
const serializedInstance = serializeToJSObject(instance);
instances.push({
...serializeToJSObject(instance),
...serializedInstance,
// Replace persistentUuid by id:
persistentUuid: instance.getPersistentUuid(),
id: instance.getPersistentUuid().slice(0, 10),
// For now, don't expose these:
initialVariables: undefined,
numberProperties: undefined,
stringProperties: undefined,
});
}
);
@@ -1209,36 +1205,152 @@ const describeInstances: EditorFunction = {
return {
success: true,
instances: instances,
instancesForSceneNamed: scene_name,
};
},
};
const iterateOnInstances = (initialInstances, callback) => {
const instanceGetter = new gd.InitialInstanceJSFunctor();
// $FlowFixMe - invoke is not writable
instanceGetter.invoke = instancePtr => {
// $FlowFixMe - wrapPointer is not exposed
const instance: gdInitialInstance = gd.wrapPointer(
instancePtr,
gd.InitialInstance
);
callback(instance);
};
// $FlowFixMe - JSFunctor is incompatible with Functor
initialInstances.iterateOverInstances(instanceGetter);
instanceGetter.delete();
};
/**
* Places a new 2D instance in a scene
* Places new instance(s), or move/erase existing instances, of an existing object onto a specified 2D layer
* within a scene using a virtual brush at given X, Y coordinates.
* Can also be used to resize, rotate, change opacity or Z order of existing 2D instance(s).
* Existing instances identifiers can be found by calling `describe_instances` (`id` field for each instance).
*/
const put2dInstance: EditorFunction = {
const put2dInstances: EditorFunction = {
renderForEditor: ({ args }) => {
const scene_name = extractRequiredString(args, 'scene_name');
const object_name = extractRequiredString(args, 'object_name');
const layer_name = extractRequiredString(args, 'layer_name');
const x = extractRequiredNumber(args, 'x');
const y = extractRequiredNumber(args, 'y');
const brush_kind = extractRequiredString(args, 'brush_kind');
const brush_position = SafeExtractor.extractStringProperty(
args,
'brush_position'
);
const existing_instance_ids = SafeExtractor.extractStringProperty(
args,
'existing_instance_ids'
);
const existingInstanceIds = existing_instance_ids
? existing_instance_ids.split(',')
: [];
const new_instances_count = SafeExtractor.extractNumberProperty(
args,
'new_instances_count'
);
const newInstancesCount =
!new_instances_count && existingInstanceIds.length === 0
? 1
: new_instances_count;
return {
text: (
<Trans>
Add instance of object {object_name} at position {x};{y} (layer:{' '}
{layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
const existingInstanceCount = existing_instance_ids
? existing_instance_ids.split(',').length
: 0;
const brushPosition = brush_position
? brush_position.split(',').map(Number)
: null;
if (brush_kind === 'erase') {
return {
text: (
<Trans>
Erase {existingInstanceCount} instance(s) of object {object_name}{' '}
(layer: {layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
}
if (existingInstanceIds.length === 0) {
return {
text: (
<Trans>
Add {newInstancesCount} instance(s) of object {object_name} at{' '}
{brushPosition ? (
brushPosition.join(', ')
) : (
<Trans>scene center</Trans>
)}{' '}
(layer: {layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
} else if (newInstancesCount === 0) {
return {
text: (
<Trans>
Move {existingInstanceCount} instance(s) of object {object_name} to{' '}
{brushPosition ? (
brushPosition.join(', ')
) : (
<Trans>scene center</Trans>
)}{' '}
(layer: {layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
} else {
return {
text: (
<Trans>
Add {newInstancesCount} instance(s) and move {existingInstanceCount}{' '}
instance(s) of object {object_name} to{' '}
{brushPosition ? (
brushPosition.join(', ')
) : (
<Trans>scene center</Trans>
)}{' '}
(layer: {layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
}
},
launchFunction: async ({ project, args }) => {
const scene_name = extractRequiredString(args, 'scene_name');
const object_name = extractRequiredString(args, 'object_name');
const layer_name = extractRequiredString(args, 'layer_name');
const x = extractRequiredNumber(args, 'x');
const y = extractRequiredNumber(args, 'y');
const brush_kind = extractRequiredString(args, 'brush_kind');
const brush_position = SafeExtractor.extractStringProperty(
args,
'brush_position'
);
const brush_size = SafeExtractor.extractStringProperty(args, 'brush_size');
const brush_end_position = SafeExtractor.extractNumberProperty(
args,
'brush_end_position'
);
const existing_instance_ids = SafeExtractor.extractStringProperty(
args,
'existing_instance_ids'
);
const new_instances_count = SafeExtractor.extractNumberProperty(
args,
'new_instances_count'
);
const instances_z_order = SafeExtractor.extractNumberProperty(
args,
'instances_z_order'
);
const instances_size = SafeExtractor.extractStringProperty(
args,
'instances_size'
);
if (!project.hasLayoutNamed(scene_name)) {
return makeGenericFailure(`Scene not found: "${scene_name}".`);
@@ -1260,48 +1372,343 @@ const put2dInstance: EditorFunction = {
);
}
const existingInstanceIds = existing_instance_ids
? existing_instance_ids.split(',')
: [];
const initialInstances = layout.getInitialInstances();
const instance = initialInstances.insertNewInitialInstance();
instance.setObjectName(object_name);
instance.setLayer(layer_name);
instance.setX(x);
instance.setY(y);
if (brush_kind === 'erase') {
const brushPosition: Array<number> | null = brush_position
? brush_position.split(',').map(Number)
: null;
const brushSize = brush_size ? Number(brush_size) : 0;
return makeGenericSuccess(
`Added instance of object "${object_name}" at position (${x}, ${y}) on layer "${layer_name ||
'base'}"`
);
// Iterate on existing instances and remove them, and/or those inside the brush radius.
const instancesToDelete = new Set();
iterateOnInstances(initialInstances, instance => {
if (instance.getLayer() !== layer_name) return;
if (instance.getObjectName() !== object_name) return;
if (
existingInstanceIds.some(id =>
instance.getPersistentUuid().startsWith(id)
)
) {
instancesToDelete.add(instance);
return;
}
if (!brushPosition) return;
if (brushSize === 0) {
if (
instance.getX() === brushPosition[0] &&
instance.getY() === brushPosition[1]
) {
instancesToDelete.add(instance);
return;
}
} else {
const distance = Math.sqrt(
Math.pow(instance.getX() - brushPosition[0], 2) +
Math.pow(instance.getY() - brushPosition[1], 2)
);
if (distance <= brushSize) {
instancesToDelete.add(instance);
return;
}
}
});
instancesToDelete.forEach(instance => {
initialInstances.removeInstance(instance);
});
return makeGenericSuccess(
`Erased ${instancesToDelete.size} instance${
instancesToDelete.size > 1 ? 's' : ''
} of object "${object_name}" on layer "${layer_name || 'base'}"`
);
} else {
const brushPosition: Array<number> = brush_position
? brush_position.split(',').map(Number)
: [
project.getGameResolutionWidth() / 2,
project.getGameResolutionHeight() / 2,
];
const brushSize = brush_size ? Number(brush_size) : 0;
const brushEndPosition = brush_end_position
? brush_end_position.split(',').map(Number)
: null;
// Compute the number of instances to create.
const rowCount = SafeExtractor.extractNumberProperty(args, 'row_count');
const columnCount = SafeExtractor.extractNumberProperty(
args,
'column_count'
);
let newInstancesCount =
new_instances_count !== null ? new_instances_count : 0;
if (newInstancesCount === 0 && existingInstanceIds.length === 0) {
newInstancesCount =
rowCount && columnCount ? rowCount * columnCount : 1;
}
// Create the array of existing instances to move/modify, and new instances to create.
const modifiedAndCreatedInstances: Array<gdInitialInstance> = [];
iterateOnInstances(initialInstances, instance => {
if (instance.getLayer() !== layer_name) return;
if (instance.getObjectName() !== object_name) return;
if (
existingInstanceIds.some(id =>
instance.getPersistentUuid().startsWith(id)
)
) {
modifiedAndCreatedInstances.push(instance);
}
});
for (let i = 0; i < newInstancesCount; i++) {
const instance = initialInstances.insertNewInitialInstance();
instance.setObjectName(object_name);
instance.setLayer(layer_name);
modifiedAndCreatedInstances.push(instance);
}
// Paint the new/modified instances with the brush.
if (brush_kind === 'line') {
const instancesCount = modifiedAndCreatedInstances.length;
if (brushPosition && brushEndPosition) {
const deltaX =
instancesCount > 1
? (brushEndPosition[0] - brushPosition[0]) / (instancesCount - 1)
: 0;
const deltaY =
instancesCount > 1
? (brushEndPosition[1] - brushPosition[1]) / (instancesCount - 1)
: 0;
modifiedAndCreatedInstances.forEach((instance, i) => {
instance.setX(brushPosition[0] + i * deltaX);
instance.setY(brushPosition[1] + i * deltaY);
});
}
} else if (brush_kind === 'grid') {
const instancesCount = modifiedAndCreatedInstances.length;
if (brushPosition && brushEndPosition) {
// Naively auto-compute the grid column and row count if not specified.
const gridRowCount =
rowCount || Math.floor(Math.sqrt(instancesCount));
const gridRowSize =
(brushEndPosition[0] - brushPosition[0]) / gridRowCount;
const gridColumnCount =
columnCount || Math.ceil(instancesCount / gridRowCount);
const gridColumnSize =
(brushEndPosition[1] - brushPosition[1]) / gridColumnCount;
modifiedAndCreatedInstances.forEach((instance, i) => {
const row = Math.floor(i / columnCount);
const column = i % columnCount;
instance.setX(brushPosition[0] + column * gridColumnSize);
instance.setY(brushPosition[1] + row * gridRowSize);
});
}
} else if (brush_kind === 'random_in_circle') {
modifiedAndCreatedInstances.forEach(instance => {
const randomRadius = Math.random() * brushSize;
const randomAngle = Math.random() * 2 * Math.PI;
instance.setX(
brushPosition[0] + randomRadius * Math.cos(randomAngle)
);
instance.setY(
brushPosition[1] + randomRadius * Math.sin(randomAngle)
);
});
} else {
if (brush_kind !== 'point') {
console.warn(
'Unknown brush kind: ' +
brush_kind +
" - assuming it's point brush instead."
);
}
modifiedAndCreatedInstances.forEach(instance => {
instance.setX(brushPosition[0]);
instance.setY(brushPosition[1]);
});
}
const instancesSize = instances_size
? instances_size.split(',').map(Number)
: null;
const instancesRotation = SafeExtractor.extractNumberProperty(
args,
'instances_rotation'
);
const instancesOpacity = SafeExtractor.extractNumberProperty(
args,
'instances_opacity'
);
modifiedAndCreatedInstances.forEach(instance => {
if (instancesSize) {
instance.setHasCustomSize(true);
instance.setCustomWidth(instancesSize[0]);
instance.setCustomHeight(instancesSize[1]);
}
if (instances_z_order !== null) {
instance.setZOrder(instances_z_order);
}
if (instancesRotation !== null) {
instance.setAngle(instancesRotation);
}
if (instancesOpacity !== null) {
instance.setOpacity(instancesOpacity);
}
});
return makeGenericSuccess(
`Added ${newInstancesCount} instance${
newInstancesCount > 1 ? 's' : ''
} of object "${object_name}" using ${brush_kind} brush at ${brush_position.join(
', '
)} on layer "${layer_name || 'base'}"`
);
}
},
};
/**
* Places a new 3D instance in a scene
* Places new instance(s), or move/erase existing instances, of an existing object
* onto a specified 3D layer within a scene using a virtual brush at given X, Y, Z coordinates.
* Can also be used to resize, rotate existing 3D instance(s).
* Existing instances identifiers can be found by calling `describe_instances` (`id` field for each instance).
*/
const put3dInstance: EditorFunction = {
const put3dInstances: EditorFunction = {
renderForEditor: ({ args }) => {
const scene_name = extractRequiredString(args, 'scene_name');
const object_name = extractRequiredString(args, 'object_name');
const layer_name = extractRequiredString(args, 'layer_name');
const x = extractRequiredNumber(args, 'x');
const y = extractRequiredNumber(args, 'y');
const z = extractRequiredNumber(args, 'z');
return {
text: (
<Trans>
Add instance of object {object_name} at position {x};{y};{z} (layer:{' '}
{layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
const brush_kind = extractRequiredString(args, 'brush_kind');
const brush_position = SafeExtractor.extractStringProperty(
args,
'brush_position'
);
const existing_instance_ids = SafeExtractor.extractStringProperty(
args,
'existing_instance_ids'
);
const existingInstanceIds = existing_instance_ids
? existing_instance_ids.split(',')
: [];
const new_instances_count = SafeExtractor.extractNumberProperty(
args,
'new_instances_count'
);
const newInstancesCount =
!new_instances_count && existingInstanceIds.length === 0
? 1
: new_instances_count;
const existingInstanceCount = existing_instance_ids
? existing_instance_ids.split(',').length
: 0;
const brushPosition = brush_position
? brush_position.split(',').map(Number)
: null;
if (brush_kind === 'erase') {
return {
text: (
<Trans>
Erase {existingInstanceCount} instance(s) of object {object_name}{' '}
(layer: {layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
}
if (existingInstanceIds.length === 0) {
return {
text: (
<Trans>
Add {newInstancesCount} instance(s) of object {object_name} at{' '}
{brushPosition ? (
brushPosition.join(', ')
) : (
<Trans>scene center</Trans>
)}{' '}
(layer: {layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
} else if (newInstancesCount === 0) {
return {
text: (
<Trans>
Move {existingInstanceCount} instance(s) of object {object_name} to{' '}
{brushPosition ? (
brushPosition.join(', ')
) : (
<Trans>scene center</Trans>
)}{' '}
(layer: {layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
} else {
return {
text: (
<Trans>
Add {newInstancesCount} instance(s) and move {existingInstanceCount}{' '}
instance(s) of object {object_name} to{' '}
{brushPosition ? (
brushPosition.join(', ')
) : (
<Trans>scene center</Trans>
)}{' '}
(layer: {layer_name || 'base'}) in scene {scene_name}.
</Trans>
),
};
}
},
launchFunction: async ({ project, args }) => {
const scene_name = extractRequiredString(args, 'scene_name');
const object_name = extractRequiredString(args, 'object_name');
const layer_name = extractRequiredString(args, 'layer_name');
const x = extractRequiredNumber(args, 'x');
const y = extractRequiredNumber(args, 'y');
const z = extractRequiredNumber(args, 'z');
const brush_kind = extractRequiredString(args, 'brush_kind');
const brush_position = SafeExtractor.extractStringProperty(
args,
'brush_position'
);
const brush_size = SafeExtractor.extractNumberProperty(args, 'brush_size');
const brush_end_position = SafeExtractor.extractStringProperty(
args,
'brush_end_position'
);
const existing_instance_ids = SafeExtractor.extractStringProperty(
args,
'existing_instance_ids'
);
const new_instances_count = SafeExtractor.extractNumberProperty(
args,
'new_instances_count'
);
const instances_size = SafeExtractor.extractStringProperty(
args,
'instances_size'
);
const instances_rotation = SafeExtractor.extractStringProperty(
args,
'instances_rotation'
);
if (!project.hasLayoutNamed(scene_name)) {
return makeGenericFailure(`Scene not found: "${scene_name}".`);
@@ -1323,19 +1730,195 @@ const put3dInstance: EditorFunction = {
);
}
const existingInstanceIds = existing_instance_ids
? existing_instance_ids.split(',')
: [];
const initialInstances = layout.getInitialInstances();
const instance = initialInstances.insertNewInitialInstance();
instance.setObjectName(object_name);
instance.setLayer(layer_name);
instance.setX(x);
instance.setY(y);
instance.setZ(z);
if (brush_kind === 'erase') {
const brushPosition: Array<number> | null = brush_position
? brush_position.split(',').map(Number)
: null;
const brushSize = brush_size || 0;
return makeGenericSuccess(
`Added 3D instance of object "${object_name}" at position (${x}, ${y}, ${z}) on layer "${layer_name ||
'base'}"`
);
// Iterate on existing instances and remove them, and/or those inside the brush radius.
const instancesToDelete = new Set();
iterateOnInstances(initialInstances, instance => {
if (instance.getLayer() !== layer_name) return;
if (instance.getObjectName() !== object_name) return;
if (
existingInstanceIds.some(id =>
instance.getPersistentUuid().startsWith(id)
)
) {
instancesToDelete.add(instance);
return;
}
if (!brushPosition) return;
if (brushSize <= 0) {
if (
instance.getX() === brushPosition[0] &&
instance.getY() === brushPosition[1] &&
instance.getZ() === brushPosition[2]
) {
instancesToDelete.add(instance);
return;
}
} else {
const distance = Math.sqrt(
Math.pow(instance.getX() - brushPosition[0], 2) +
Math.pow(instance.getY() - brushPosition[1], 2) +
Math.pow(instance.getZ() - brushPosition[2], 2)
);
if (distance <= brushSize) {
instancesToDelete.add(instance);
return;
}
}
});
instancesToDelete.forEach(instance => {
initialInstances.removeInstance(instance);
});
return makeGenericSuccess(
`Erased ${instancesToDelete.size} instance${
instancesToDelete.size > 1 ? 's' : ''
} of object "${object_name}" on layer "${layer_name || 'base'}"`
);
} else {
const brushPosition: Array<number> = brush_position
? brush_position.split(',').map(Number)
: [
project.getGameResolutionWidth() / 2,
project.getGameResolutionHeight() / 2,
0,
];
const brushSize = brush_size || 0;
const brushEndPosition: Array<number> | null = brush_end_position
? brush_end_position.split(',').map(Number)
: null;
let newInstancesCount =
new_instances_count !== null ? new_instances_count : 0;
if (newInstancesCount === 0 && existingInstanceIds.length === 0) {
newInstancesCount = 1;
}
// Create the array of existing instances to move/modify, and new instances to create.
const modifiedAndCreatedInstances: Array<gdInitialInstance> = [];
iterateOnInstances(initialInstances, instance => {
if (instance.getLayer() !== layer_name) return;
if (instance.getObjectName() !== object_name) return;
if (
existingInstanceIds.some(id =>
instance.getPersistentUuid().startsWith(id)
)
) {
modifiedAndCreatedInstances.push(instance);
}
});
for (let i = 0; i < newInstancesCount; i++) {
const instance = initialInstances.insertNewInitialInstance();
instance.setObjectName(object_name);
instance.setLayer(layer_name);
modifiedAndCreatedInstances.push(instance);
}
// Paint the new/modified instances with the brush.
if (brush_kind === 'line') {
const instancesCount = modifiedAndCreatedInstances.length;
if (brushPosition && brushEndPosition) {
const deltaX =
instancesCount > 1
? (brushEndPosition[0] - brushPosition[0]) / (instancesCount - 1)
: 0;
const deltaY =
instancesCount > 1
? (brushEndPosition[1] - brushPosition[1]) / (instancesCount - 1)
: 0;
const deltaZ =
instancesCount > 1
? (brushEndPosition[2] - brushPosition[2]) / (instancesCount - 1)
: 0;
modifiedAndCreatedInstances.forEach((instance, i) => {
instance.setX(brushPosition[0] + i * deltaX);
instance.setY(brushPosition[1] + i * deltaY);
instance.setZ(brushPosition[2] + i * deltaZ);
});
}
} else if (brush_kind === 'random_in_sphere') {
modifiedAndCreatedInstances.forEach(instance => {
if (!brushPosition) return;
const randomRadius = Math.random() * brushSize;
const randomTheta = Math.random() * 2 * Math.PI; // Azimuthal angle
const randomPhi = Math.acos(2 * Math.random() - 1); // Polar angle
instance.setX(
brushPosition[0] +
randomRadius * Math.sin(randomPhi) * Math.cos(randomTheta)
);
instance.setY(
brushPosition[1] +
randomRadius * Math.sin(randomPhi) * Math.sin(randomTheta)
);
instance.setZ(brushPosition[2] + randomRadius * Math.cos(randomPhi));
});
} else {
if (brush_kind !== 'point') {
console.warn(
'Unknown brush kind: ' +
brush_kind +
" - assuming it's point brush instead."
);
}
modifiedAndCreatedInstances.forEach(instance => {
if (!brushPosition) return;
instance.setX(brushPosition[0]);
instance.setY(brushPosition[1]);
instance.setZ(brushPosition[2]);
});
}
const instancesSizeArray = instances_size
? instances_size.split(',').map(Number)
: null;
const instancesRotationArray = instances_rotation
? instances_rotation.split(',').map(coord => parseFloat(coord) || 0)
: null;
modifiedAndCreatedInstances.forEach(instance => {
if (instancesSizeArray && instancesSizeArray.length >= 3) {
instance.setHasCustomSize(true);
instance.setHasCustomDepth(true);
instance.setCustomWidth(instancesSizeArray[0]);
instance.setCustomHeight(instancesSizeArray[1]);
instance.setCustomDepth(instancesSizeArray[2]);
}
if (instancesRotationArray && instancesRotationArray.length >= 3) {
instance.setRotationX(instancesRotationArray[0]);
instance.setRotationY(instancesRotationArray[1]);
instance.setAngle(instancesRotationArray[2]);
}
});
return makeGenericSuccess(
`Added ${newInstancesCount} instance${
newInstancesCount > 1 ? 's' : ''
} of object "${object_name}" using ${brush_kind} brush at ${brushPosition.join(
', '
)}) on layer "${layer_name || 'base'}"`
);
}
},
};
@@ -1931,8 +2514,8 @@ export const editorFunctions: { [string]: EditorFunction } = {
inspect_behavior_properties: inspectBehaviorProperties,
change_behavior_property: changeBehaviorProperty,
describe_instances: describeInstances,
put_2d_instance: put2dInstance,
put_3d_instance: put3dInstance,
put_2d_instances: put2dInstances,
put_3d_instances: put3dInstances,
read_scene_events: readSceneEvents,
add_scene_events: addSceneEvents,
create_scene: createScene,