mirror of
https://github.com/Heretek-AI/GDevelop-Heretek.git
synced 2026-10-12 02:02:42 +00:00
Allow to edit layers/masks (from the Physics engines) and some other properties of behaviors in the properties panel (#9030)
This commit is contained in:
@@ -219,6 +219,13 @@ module.exports = {
|
||||
physics2Behavior.getProperties = function (behaviorContent) {
|
||||
var behaviorProperties = new gd.MapStringPropertyDescriptor();
|
||||
|
||||
// The shape decides which dimensions are meaningful and how they
|
||||
// should be labelled, so these properties adapt themselves to it.
|
||||
const shape = behaviorContent.getChild('shape').getStringValue();
|
||||
const isCircleShape = shape === 'Circle';
|
||||
const isEdgeShape = shape === 'Edge';
|
||||
const isPolygonShape = shape === 'Polygon';
|
||||
|
||||
behaviorProperties
|
||||
.getOrCreate('bodyType')
|
||||
.setValue(behaviorContent.getChild('bodyType').getStringValue())
|
||||
@@ -283,12 +290,18 @@ module.exports = {
|
||||
.getOrCreate('shape')
|
||||
.setValue(behaviorContent.getChild('shape').getStringValue())
|
||||
.setType('Choice')
|
||||
.setLabel('Shape')
|
||||
.setLabel(_('Shape'))
|
||||
.setDescription(
|
||||
_(
|
||||
'The shape used for collisions. It gives their meaning to the dimension properties. A polygon is defined by its "Vertices" and positioned according to "Polygon origin".'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.addChoice('Box', _('Box'))
|
||||
.addChoice('Circle', _('Circle'))
|
||||
.addChoice('Edge', _('Edge'))
|
||||
.addChoice('Polygon', _('Polygon'));
|
||||
.addChoice('Polygon', _('Polygon'))
|
||||
.setHasImpactOnOtherProperties(true);
|
||||
behaviorProperties
|
||||
.getOrCreate('shapeDimensionA')
|
||||
.setValue(
|
||||
@@ -299,9 +312,17 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape Dimension A')
|
||||
.setLabel(
|
||||
isCircleShape ? _('Radius') : isEdgeShape ? _('Length') : _('Width')
|
||||
)
|
||||
.setDescription(
|
||||
_(
|
||||
'Width of the box, radius of the circle or length of the edge. Use 0 to follow the object size.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
// A polygon is defined by its vertices instead of dimensions.
|
||||
.setHidden(isPolygonShape);
|
||||
behaviorProperties
|
||||
.getOrCreate('shapeDimensionB')
|
||||
.setValue(
|
||||
@@ -311,10 +332,20 @@ module.exports = {
|
||||
.toString(10)
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape Dimension B')
|
||||
.setMeasurementUnit(
|
||||
isEdgeShape
|
||||
? gd.MeasurementUnit.getDegreeAngle()
|
||||
: gd.MeasurementUnit.getPixel()
|
||||
)
|
||||
.setLabel(isEdgeShape ? _('Angle') : _('Height'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Height of the box, or angle of the edge in degrees. Not used by a circle. Use 0 to follow the object height.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
// A circle only needs a radius and a polygon uses its vertices.
|
||||
.setHidden(isPolygonShape || isCircleShape);
|
||||
behaviorProperties
|
||||
.getOrCreate('shapeOffsetX')
|
||||
.setValue(
|
||||
@@ -322,9 +353,14 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape Offset X')
|
||||
.setLabel(_('Shape offset X'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Offset of the collision shape relative to the object center, on the X axis.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setAdvanced(true);
|
||||
behaviorProperties
|
||||
.getOrCreate('shapeOffsetY')
|
||||
.setValue(
|
||||
@@ -332,9 +368,14 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape Offset Y')
|
||||
.setLabel(_('Shape offset Y'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Offset of the collision shape relative to the object center, on the Y axis.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setAdvanced(true);
|
||||
behaviorProperties
|
||||
.getOrCreate('polygonOrigin')
|
||||
.setValue(
|
||||
@@ -343,12 +384,16 @@ module.exports = {
|
||||
: 'Center'
|
||||
)
|
||||
.setType('Choice')
|
||||
.setLabel('Polygon Origin')
|
||||
.setLabel(_('Polygon origin'))
|
||||
.setDescription(
|
||||
_('Point of the object the polygon vertices are relative to.')
|
||||
)
|
||||
.addChoice('Center', _('Center'))
|
||||
.addChoice('Origin', _('Origin'))
|
||||
.addChoice('TopLeft', _('TopLeft'))
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
// Only used by the "Polygon" shape.
|
||||
.setHidden(!isPolygonShape);
|
||||
behaviorProperties
|
||||
.getOrCreate('vertices')
|
||||
.setValue(
|
||||
@@ -434,17 +479,31 @@ module.exports = {
|
||||
behaviorProperties
|
||||
.getOrCreate('layers')
|
||||
.setValue(behaviorContent.getChild('layers').getIntValue().toString(10))
|
||||
.setType('Number')
|
||||
.setLabel('Layers')
|
||||
.setType('Bitmask')
|
||||
.addExtraInfo('bitCount=16')
|
||||
.setLabel(_('Layers'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Layers the object belongs to, as a bitmask: layer 1 is 1, layer 2 is 2, layer 3 is 4, and so on up to layer 16 which is 32768.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setGroup(_('Collision filtering'))
|
||||
.setAdvanced(true);
|
||||
behaviorProperties
|
||||
.getOrCreate('masks')
|
||||
.setValue(behaviorContent.getChild('masks').getIntValue().toString(10))
|
||||
.setType('Number')
|
||||
.setLabel('Masks')
|
||||
.setType('Bitmask')
|
||||
.addExtraInfo('bitCount=16')
|
||||
.setLabel(_('Masks'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Layers the object can collide with, as a bitmask: layer 1 is 1, layer 2 is 2, layer 3 is 4, and so on up to layer 16 which is 32768.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setGroup(_('Collision filtering'))
|
||||
.setAdvanced(true);
|
||||
|
||||
return behaviorProperties;
|
||||
};
|
||||
|
||||
@@ -285,6 +285,15 @@ module.exports = {
|
||||
behavior.getProperties = function (behaviorContent) {
|
||||
const behaviorProperties = new gd.MapStringPropertyDescriptor();
|
||||
|
||||
// The shape decides which dimensions are meaningful and how they
|
||||
// should be labelled, and the body type decides which collision
|
||||
// layers are used, so these properties adapt themselves to them.
|
||||
const shape = behaviorContent.getChild('shape').getStringValue();
|
||||
const isBoxShape = shape === 'Box';
|
||||
const isMeshShape = shape === 'Mesh';
|
||||
const isStaticBody =
|
||||
behaviorContent.getChild('bodyType').getStringValue() === 'Static';
|
||||
|
||||
behaviorProperties
|
||||
.getOrCreate('object3D')
|
||||
.setValue(behaviorContent.getChild('object3D').getStringValue())
|
||||
@@ -343,13 +352,19 @@ module.exports = {
|
||||
.getOrCreate('shape')
|
||||
.setValue(behaviorContent.getChild('shape').getStringValue())
|
||||
.setType('Choice')
|
||||
.setLabel('Shape')
|
||||
.setLabel(_('Shape'))
|
||||
.setDescription(
|
||||
_(
|
||||
'The shape used for collisions. It gives their meaning to the dimension properties. A capsule or a cylinder is extended along the axis given by "Shape orientation" and a mesh uses the model given by "Simplified 3D model".'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.addChoice('Box', _('Box'))
|
||||
.addChoice('Capsule', _('Capsule'))
|
||||
.addChoice('Sphere', _('Sphere'))
|
||||
.addChoice('Cylinder', _('Cylinder'))
|
||||
.addChoice('Mesh', _('Mesh (works for Static only)'));
|
||||
.addChoice('Mesh', _('Mesh (works for Static only)'))
|
||||
.setHasImpactOnOtherProperties(true);
|
||||
behaviorProperties
|
||||
.getOrCreate('meshShapeResourceName')
|
||||
.setValue(
|
||||
@@ -359,8 +374,8 @@ module.exports = {
|
||||
.addExtraInfo('model3D')
|
||||
.setLabel(_('Simplified 3D model'))
|
||||
.setDescription(_("Leave empty to use object's one"))
|
||||
// Hidden as required to be changed in the full editor.
|
||||
.setHidden(true)
|
||||
// Only used by the "Mesh" shape.
|
||||
.setHidden(!isMeshShape)
|
||||
.setHasImpactOnOtherProperties(true);
|
||||
behaviorProperties
|
||||
.getOrCreate('shapeOrientation')
|
||||
@@ -368,11 +383,16 @@ module.exports = {
|
||||
behaviorContent.getChild('shapeOrientation').getStringValue()
|
||||
)
|
||||
.setType('Choice')
|
||||
.setLabel('Shape orientation')
|
||||
.setLabel(_('Shape orientation'))
|
||||
.setDescription(
|
||||
_('Axis along which the capsule or cylinder is extended.')
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.addChoice('Z', _('Z'))
|
||||
.addChoice('Y', _('Y'))
|
||||
.addChoice('X', _('X'));
|
||||
.addChoice('X', _('X'))
|
||||
// Only capsules and cylinders can be oriented.
|
||||
.setHidden(shape !== 'Capsule' && shape !== 'Cylinder');
|
||||
behaviorProperties
|
||||
.getOrCreate('shapeDimensionA')
|
||||
.setValue(
|
||||
@@ -383,9 +403,15 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape Dimension A')
|
||||
.setLabel(isBoxShape ? _('Width') : _('Radius'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Width of the box, or radius of the sphere, capsule or cylinder. Use 0 to follow the object size.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
// The "Mesh" shape uses a 3D model instead of dimensions.
|
||||
.setHidden(isMeshShape);
|
||||
behaviorProperties
|
||||
.getOrCreate('shapeDimensionB')
|
||||
.setValue(
|
||||
@@ -396,9 +422,15 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape Dimension B')
|
||||
.setLabel(isBoxShape ? _('Height') : _('Depth'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Height of the box, or size of the capsule or cylinder along its orientation axis. Not used by a sphere. Use 0 to follow the object size.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
// A sphere only needs a radius and a mesh uses a 3D model.
|
||||
.setHidden(isMeshShape || shape === 'Sphere');
|
||||
behaviorProperties
|
||||
.getOrCreate('shapeDimensionC')
|
||||
.setValue(
|
||||
@@ -409,9 +441,15 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape Dimension C')
|
||||
.setLabel(_('Depth'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Depth of the box. Only used by a box. Use 0 to follow the object depth.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
// Only a box has a third dimension to configure.
|
||||
.setHidden(!isBoxShape);
|
||||
if (!behaviorContent.hasChild('shapeOffsetX')) {
|
||||
behaviorContent.addChild('shapeOffsetX').setDoubleValue(0);
|
||||
}
|
||||
@@ -425,10 +463,14 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape offset X')
|
||||
.setLabel(_('Shape offset X'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Offset of the collision shape relative to the object center, on the X axis.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setAdvanced(true)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setAdvanced(true);
|
||||
if (!behaviorContent.hasChild('shapeOffsetY')) {
|
||||
behaviorContent.addChild('shapeOffsetY').setDoubleValue(0);
|
||||
}
|
||||
@@ -442,9 +484,14 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape offset Y')
|
||||
.setLabel(_('Shape offset Y'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Offset of the collision shape relative to the object center, on the Y axis.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setAdvanced(true);
|
||||
if (!behaviorContent.hasChild('shapeOffsetZ')) {
|
||||
behaviorContent.addChild('shapeOffsetZ').setDoubleValue(0);
|
||||
}
|
||||
@@ -458,10 +505,14 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Shape offset Z')
|
||||
.setLabel(_('Shape offset Z'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Offset of the collision shape relative to the object center, on the Z axis.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setAdvanced(true)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setAdvanced(true);
|
||||
if (!behaviorContent.hasChild('massCenterOffsetX')) {
|
||||
behaviorContent.addChild('massCenterOffsetX').setDoubleValue(0);
|
||||
}
|
||||
@@ -475,10 +526,14 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Center of mass offset X')
|
||||
.setLabel(_('Center of mass X'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Offset of the center of mass relative to the object center, on the X axis.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setAdvanced(true)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setAdvanced(true);
|
||||
if (!behaviorContent.hasChild('massCenterOffsetY')) {
|
||||
behaviorContent.addChild('massCenterOffsetY').setDoubleValue(0);
|
||||
}
|
||||
@@ -492,10 +547,14 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Center of mass offset Y')
|
||||
.setLabel(_('Center of mass Y'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Offset of the center of mass relative to the object center, on the Y axis.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setAdvanced(true)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setAdvanced(true);
|
||||
if (!behaviorContent.hasChild('massCenterOffsetZ')) {
|
||||
behaviorContent.addChild('massCenterOffsetZ').setDoubleValue(0);
|
||||
}
|
||||
@@ -509,10 +568,14 @@ module.exports = {
|
||||
)
|
||||
.setType('Number')
|
||||
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
|
||||
.setLabel('Center of mass offset Z')
|
||||
.setLabel(_('Center of mass Z'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Offset of the center of mass relative to the object center, on the Z axis.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setAdvanced(true)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setAdvanced(true);
|
||||
behaviorProperties
|
||||
.getOrCreate('density')
|
||||
.setValue(
|
||||
@@ -627,19 +690,39 @@ module.exports = {
|
||||
.setValue(
|
||||
behaviorContent.getChild('layers').getIntValue().toString(10)
|
||||
)
|
||||
.setType('Number')
|
||||
.setLabel('Layers')
|
||||
.setType('Bitmask')
|
||||
// Static objects use the layers 1 to 4 and moving ones the layers
|
||||
// 5 to 8, so that they never register in the wrong layer group.
|
||||
.addExtraInfo('bitCount=4')
|
||||
.addExtraInfo(isStaticBody ? 'firstBit=0' : 'firstBit=4')
|
||||
.setLabel(_('Layers'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Layers the object belongs to, as a bitmask. Static objects use the layers 1 to 4 (1, 2, 4 and 8) and moving ones the layers 5 to 8 (16, 32, 64 and 128).'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setGroup(_('Collision filtering'))
|
||||
.setAdvanced(true);
|
||||
behaviorProperties
|
||||
.getOrCreate('masks')
|
||||
.setValue(
|
||||
behaviorContent.getChild('masks').getIntValue().toString(10)
|
||||
)
|
||||
.setType('Number')
|
||||
.setLabel('Masks')
|
||||
.setType('Bitmask')
|
||||
.addExtraInfo('bitCount=8')
|
||||
.setLabel(_('Masks'))
|
||||
.setDescription(
|
||||
_(
|
||||
'Layers the object can collide with, as a bitmask: layer 1 is 1, layer 2 is 2, layer 3 is 4, and so on up to layer 8 which is 128. Not used by static objects, which accept every collision.'
|
||||
)
|
||||
)
|
||||
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
|
||||
.setHidden(true); // Hidden as required to be changed in the full editor.
|
||||
.setGroup(_('Collision filtering'))
|
||||
.setAdvanced(true)
|
||||
// Static objects accept every collision: it's the mask of the moving
|
||||
// objects that matters.
|
||||
.setHidden(isStaticBody);
|
||||
|
||||
return behaviorProperties;
|
||||
};
|
||||
|
||||
@@ -240,6 +240,8 @@ const translateTypeToHumanReadableDescription = type => {
|
||||
if (caseInsensitiveCheck(type, 'boolean')) return '🔘 Boolean';
|
||||
if (caseInsensitiveCheck(type, 'color')) return '🎨 Color';
|
||||
if (caseInsensitiveCheck(type, 'resource')) return '🗂️ Resource';
|
||||
if (caseInsensitiveCheck(type, 'bitmask'))
|
||||
return '🔢 Bitmask, one bit per option (Number)';
|
||||
|
||||
return type;
|
||||
};
|
||||
@@ -1168,7 +1170,9 @@ const generateExtensionRawText = (
|
||||
].join(' ');
|
||||
}),
|
||||
'',
|
||||
effectMetadata.getHelpPath() ? generateReadMoreLink(effectMetadata.getHelpPath()) : '',
|
||||
effectMetadata.getHelpPath()
|
||||
? generateReadMoreLink(effectMetadata.getHelpPath())
|
||||
: '',
|
||||
'',
|
||||
].join(`\n`),
|
||||
};
|
||||
|
||||
@@ -17,13 +17,14 @@ import MeasurementUnitDocumentation from '../../../PropertiesEditor/MeasurementU
|
||||
import ShapePreview from './ShapePreview';
|
||||
import PolygonEditor from './PolygonEditor';
|
||||
import { type BehaviorEditorProps } from '../BehaviorEditorProps.flow';
|
||||
import Text from '../../../UI/Text';
|
||||
import DismissableAlertMessage from '../../../UI/DismissableAlertMessage';
|
||||
import { ResponsiveLineStackLayout } from '../../../UI/Layout';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../../UI/Layout';
|
||||
import { CompactBitmaskField } from '../../../UI/CompactBitmaskField';
|
||||
import EmptyMessage from '../../../UI/EmptyMessage';
|
||||
import useForceUpdate from '../../../Utils/UseForceUpdate';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import CircledInfo from '../../../UI/CustomSvgIcons/SmallCircledInfo';
|
||||
@@ -129,38 +130,6 @@ export const UnitAdornment = (props: {|
|
||||
);
|
||||
};
|
||||
|
||||
const BitGroupEditor = (props: {|
|
||||
bits: Array<boolean>,
|
||||
onChange: (index: number, value: boolean) => void,
|
||||
|}) => {
|
||||
return (
|
||||
<div style={{ overflowX: 'auto', flex: 1 }}>
|
||||
<ButtonGroup disableElevation fullWidth>
|
||||
{props.bits.map((bit, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant={bit ? 'contained' : 'outlined'}
|
||||
color={bit ? 'primary' : 'default'}
|
||||
onClick={() => props.onChange(index, !bit)}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isBitEnabled = (bitsValue: number, pos: number) => {
|
||||
return !!(bitsValue & (1 << pos));
|
||||
};
|
||||
|
||||
const enableBit = (bitsValue: number, pos: number, enable: boolean) => {
|
||||
if (enable) bitsValue |= 1 << pos;
|
||||
else bitsValue &= ~(1 << pos);
|
||||
return bitsValue;
|
||||
};
|
||||
|
||||
const Physics2Editor = (props: Props): React.Node => {
|
||||
const { current: resourcesLoader } = React.useRef(ResourcesLoader);
|
||||
const [image, setImage] = React.useState('');
|
||||
@@ -178,7 +147,6 @@ const Physics2Editor = (props: Props): React.Node => {
|
||||
);
|
||||
|
||||
const properties = behavior.getProperties();
|
||||
const bits = Array(16).fill(null);
|
||||
const shape = properties.get('shape').getValue();
|
||||
const layersValues = parseInt(properties.get('layers').getValue(), 10);
|
||||
const masksValues = parseInt(properties.get('masks').getValue(), 10);
|
||||
@@ -247,20 +215,14 @@ const Physics2Editor = (props: Props): React.Node => {
|
||||
/>
|
||||
</Line>
|
||||
<ResponsiveLineStackLayout>
|
||||
{shape !== 'Polygon' && (
|
||||
{/* Labels and visibility are given by the properties themselves,
|
||||
as they depend on the shape. */}
|
||||
{!properties.get('shapeDimensionA').isHidden() && (
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionA').getValue()}
|
||||
key={'shapeDimensionA'}
|
||||
floatingLabelText={
|
||||
shape === 'Circle' ? (
|
||||
<Trans>Radius</Trans>
|
||||
) : shape === 'Edge' ? (
|
||||
<Trans>Length</Trans>
|
||||
) : (
|
||||
<Trans>Width</Trans>
|
||||
)
|
||||
}
|
||||
floatingLabelText={properties.get('shapeDimensionA').getLabel()}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionA', newValue)
|
||||
@@ -271,14 +233,12 @@ const Physics2Editor = (props: Props): React.Node => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{shape !== 'Polygon' && shape !== 'Circle' && (
|
||||
{!properties.get('shapeDimensionB').isHidden() && (
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionB').getValue()}
|
||||
key={'shapeDimensionB'}
|
||||
floatingLabelText={
|
||||
shape === 'Edge' ? <Trans>Angle</Trans> : <Trans>Height</Trans>
|
||||
}
|
||||
floatingLabelText={properties.get('shapeDimensionB').getLabel()}
|
||||
min={shape === 'Edge' ? undefined : 0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionB', newValue)
|
||||
@@ -289,7 +249,7 @@ const Physics2Editor = (props: Props): React.Node => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{shape === 'Polygon' && (
|
||||
{!properties.get('polygonOrigin').isHidden() && (
|
||||
<ChoiceProperty
|
||||
properties={properties}
|
||||
propertyName={'polygonOrigin'}
|
||||
@@ -504,29 +464,27 @@ const Physics2Editor = (props: Props): React.Node => {
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<Line>
|
||||
<Text style={{ marginRight: 10 }}>
|
||||
{properties.get('layers').getLabel()}
|
||||
</Text>
|
||||
<BitGroupEditor
|
||||
bits={bits.map((_, idx) => isBitEnabled(layersValues, idx))}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(layersValues, index, value);
|
||||
updateBehaviorProperty('layers', newValue.toString(10));
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Text style={{ marginRight: 10 }}>
|
||||
{properties.get('masks').getLabel()}
|
||||
</Text>
|
||||
<BitGroupEditor
|
||||
bits={bits.map((_, idx) => isBitEnabled(masksValues, idx))}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(masksValues, index, value);
|
||||
updateBehaviorProperty('masks', newValue.toString(10));
|
||||
}}
|
||||
/>
|
||||
<Line expand>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<CompactBitmaskField
|
||||
label={properties.get('layers').getLabel()}
|
||||
markdownDescription={properties.get('layers').getDescription()}
|
||||
value={layersValues}
|
||||
bitCount={16}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('layers', newValue.toString(10))
|
||||
}
|
||||
/>
|
||||
<CompactBitmaskField
|
||||
label={properties.get('masks').getLabel()}
|
||||
markdownDescription={properties.get('masks').getDescription()}
|
||||
value={masksValues}
|
||||
bitCount={16}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('masks', newValue.toString(10))
|
||||
}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Line, Column, Spacer } from '../../../UI/Grid';
|
||||
import { Line, Column } from '../../../UI/Grid';
|
||||
import Checkbox from '../../../UI/Checkbox';
|
||||
import SemiControlledTextField from '../../../UI/SemiControlledTextField';
|
||||
import { type BehaviorEditorProps } from '../BehaviorEditorProps.flow';
|
||||
import Text from '../../../UI/Text';
|
||||
import DismissableAlertMessage from '../../../UI/DismissableAlertMessage';
|
||||
import { ResponsiveLineStackLayout } from '../../../UI/Layout';
|
||||
import {
|
||||
ColumnStackLayout,
|
||||
ResponsiveLineStackLayout,
|
||||
} from '../../../UI/Layout';
|
||||
import { CompactBitmaskField } from '../../../UI/CompactBitmaskField';
|
||||
import useForceUpdate from '../../../Utils/UseForceUpdate';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import ButtonGroup from '@material-ui/core/ButtonGroup';
|
||||
import {
|
||||
NumericProperty,
|
||||
UnitAdornment,
|
||||
@@ -57,40 +59,6 @@ const areAdvancedPropertiesModified = (
|
||||
return hasFoundModifiedAdvancedProperty;
|
||||
};
|
||||
|
||||
const BitGroupEditor = (props: {|
|
||||
bits: Array<boolean>,
|
||||
onChange: (index: number, value: boolean) => void,
|
||||
firstIndex: number,
|
||||
disabled: boolean,
|
||||
|}) => {
|
||||
return (
|
||||
<div style={{ overflowX: 'auto', flex: 1 }}>
|
||||
<ButtonGroup disableElevation fullWidth disabled={props.disabled}>
|
||||
{props.bits.map((bit, index) => (
|
||||
<Button
|
||||
key={props.firstIndex + index}
|
||||
variant={bit ? 'contained' : 'outlined'}
|
||||
color={bit ? 'primary' : 'default'}
|
||||
onClick={() => props.onChange(props.firstIndex + index, !bit)}
|
||||
>
|
||||
{props.firstIndex + index + 1}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isBitEnabled = (bitsValue: number, pos: number) => {
|
||||
return !!(bitsValue & (1 << pos));
|
||||
};
|
||||
|
||||
const enableBit = (bitsValue: number, pos: number, enable: boolean) => {
|
||||
if (enable) bitsValue |= 1 << pos;
|
||||
else bitsValue &= ~(1 << pos);
|
||||
return bitsValue;
|
||||
};
|
||||
|
||||
const Physics3DEditor = (props: Props): React.Node => {
|
||||
const {
|
||||
object,
|
||||
@@ -132,17 +100,14 @@ const Physics3DEditor = (props: Props): React.Node => {
|
||||
);
|
||||
|
||||
const properties = behavior.getProperties();
|
||||
const staticBits = Array(4).fill(null);
|
||||
const dynamicBits = Array(4).fill(null);
|
||||
const shape = properties.get('shape').getValue();
|
||||
const layersValues = parseInt(properties.get('layers').getValue(), 10);
|
||||
const masksValues = parseInt(properties.get('masks').getValue(), 10);
|
||||
|
||||
const isStatic = properties.get('bodyType').getValue() === 'Static';
|
||||
|
||||
const canShapeBeOriented =
|
||||
properties.get('shape').getValue() !== 'Sphere' &&
|
||||
properties.get('shape').getValue() !== 'Box';
|
||||
// The property is hidden for the shapes that can't be oriented.
|
||||
const canShapeBeOriented = !properties.get('shapeOrientation').isHidden();
|
||||
|
||||
// $FlowFixMe[value-as-type]
|
||||
const [gltf, setGltf] = React.useState<GLTF | null>(null);
|
||||
@@ -281,56 +246,26 @@ const Physics3DEditor = (props: Props): React.Node => {
|
||||
)}
|
||||
{shape !== 'Mesh' && (
|
||||
<ResponsiveLineStackLayout>
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionA').getValue()}
|
||||
key={'shapeDimensionA'}
|
||||
floatingLabelText={
|
||||
shape === 'Box' ? <Trans>Width</Trans> : <Trans>Radius</Trans>
|
||||
}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionA', newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get('shapeDimensionA')} />
|
||||
}
|
||||
/>
|
||||
{shape !== 'Sphere' && (
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionB').getValue()}
|
||||
key={'shapeDimensionB'}
|
||||
floatingLabelText={
|
||||
shape === 'Box' ? <Trans>Height</Trans> : <Trans>Depth</Trans>
|
||||
}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionB', newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get('shapeDimensionB')} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{shape === 'Box' && (
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get('shapeDimensionC').getValue()}
|
||||
key={'shapeDimensionC'}
|
||||
floatingLabelText={<Trans>Depth</Trans>}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('shapeDimensionC', newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get('shapeDimensionC')} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* Labels and visibility are given by the properties themselves,
|
||||
as they depend on the shape. */}
|
||||
{['shapeDimensionA', 'shapeDimensionB', 'shapeDimensionC']
|
||||
.filter(propertyName => !properties.get(propertyName).isHidden())
|
||||
.map(propertyName => (
|
||||
<SemiControlledTextField
|
||||
fullWidth
|
||||
value={properties.get(propertyName).getValue()}
|
||||
key={propertyName}
|
||||
floatingLabelText={properties.get(propertyName).getLabel()}
|
||||
min={0}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty(propertyName, newValue)
|
||||
}
|
||||
type="number"
|
||||
endAdornment={
|
||||
<UnitAdornment property={properties.get(propertyName)} />
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</ResponsiveLineStackLayout>
|
||||
)}
|
||||
{shape === 'Mesh' && (
|
||||
@@ -441,65 +376,32 @@ const Physics3DEditor = (props: Props): React.Node => {
|
||||
}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<Line>
|
||||
<Text style={{ marginRight: 10 }}>
|
||||
{properties.get('layers').getLabel()}
|
||||
</Text>
|
||||
<BitGroupEditor
|
||||
key={'static-layers'}
|
||||
firstIndex={0}
|
||||
bits={staticBits.map(
|
||||
(_, index) => isBitEnabled(layersValues, index) && isStatic
|
||||
<Line expand>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
{/* Static and moving objects don't use the same layers, so only
|
||||
the ones of the current body type are shown. */}
|
||||
<CompactBitmaskField
|
||||
label={properties.get('layers').getLabel()}
|
||||
markdownDescription={properties.get('layers').getDescription()}
|
||||
value={layersValues}
|
||||
firstBit={isStatic ? 0 : 4}
|
||||
bitCount={4}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('layers', newValue.toString(10))
|
||||
}
|
||||
/>
|
||||
{!properties.get('masks').isHidden() && (
|
||||
<CompactBitmaskField
|
||||
label={properties.get('masks').getLabel()}
|
||||
markdownDescription={properties.get('masks').getDescription()}
|
||||
value={masksValues}
|
||||
bitCount={8}
|
||||
onChange={newValue =>
|
||||
updateBehaviorProperty('masks', newValue.toString(10))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(layersValues, index, value);
|
||||
updateBehaviorProperty('layers', newValue.toString(10));
|
||||
}}
|
||||
disabled={!isStatic}
|
||||
/>
|
||||
<Spacer />
|
||||
<BitGroupEditor
|
||||
key={'dynamic-layers'}
|
||||
firstIndex={4}
|
||||
bits={dynamicBits.map(
|
||||
(_, index) => isBitEnabled(layersValues, index + 4) && !isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(layersValues, index, value);
|
||||
updateBehaviorProperty('layers', newValue.toString(10));
|
||||
}}
|
||||
disabled={isStatic}
|
||||
/>
|
||||
</Line>
|
||||
<Line>
|
||||
<Text style={{ marginRight: 10 }}>
|
||||
{properties.get('masks').getLabel()}
|
||||
</Text>
|
||||
<BitGroupEditor
|
||||
key={'static-mask'}
|
||||
firstIndex={0}
|
||||
bits={staticBits.map(
|
||||
(_, index) => isBitEnabled(masksValues, index) || isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(masksValues, index, value);
|
||||
updateBehaviorProperty('masks', newValue.toString(10));
|
||||
}}
|
||||
disabled={isStatic}
|
||||
/>
|
||||
<Spacer />
|
||||
<BitGroupEditor
|
||||
key={'dynamic-mask'}
|
||||
firstIndex={4}
|
||||
bits={dynamicBits.map(
|
||||
(_, index) => isBitEnabled(masksValues, index + 4) || isStatic
|
||||
)}
|
||||
onChange={(index, value) => {
|
||||
const newValue = enableBit(masksValues, index, value);
|
||||
updateBehaviorProperty('masks', newValue.toString(10));
|
||||
}}
|
||||
disabled={isStatic}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
<Accordion
|
||||
defaultExpanded={areAdvancedPropertiesExpandedByDefault}
|
||||
|
||||
@@ -24,6 +24,7 @@ import CompactPropertiesEditorRowField from './CompactPropertiesEditorRowField';
|
||||
import CompactToggleButtons from '../UI/CompactToggleButtons';
|
||||
import { CompactToggleField } from '../UI/CompactToggleField';
|
||||
import { CompactTextAreaField } from '../UI/CompactTextAreaField';
|
||||
import { CompactBitmaskField } from '../UI/CompactBitmaskField';
|
||||
import { CompactColorField } from '../UI/CompactColorField';
|
||||
import { rgbOrHexToRGBString } from '../Utils/ColorTransformer';
|
||||
import { CompactResourceSelectorWithThumbnail } from '../ResourcesList/CompactResourceSelectorWithThumbnail';
|
||||
@@ -399,6 +400,31 @@ const CompactPropertiesEditor = ({
|
||||
{field.renderIcon(value)}
|
||||
</IconButton>
|
||||
);
|
||||
} else if (field.valueType === 'bitmask') {
|
||||
const { setValue, firstBit, bitCount } = field;
|
||||
const mixedValues = hasMixedValues({ instances, field });
|
||||
return (
|
||||
<CompactBitmaskField
|
||||
key={field.name}
|
||||
id={field.name}
|
||||
label={getFieldLabel({ instances, field })}
|
||||
markdownDescription={getFieldDescription(field)}
|
||||
value={mixedValues ? 0 : getFieldValue({ instances, field })}
|
||||
firstBit={firstBit}
|
||||
bitCount={bitCount}
|
||||
onChange={newValue => {
|
||||
instances.forEach(i => setValue(i, newValue));
|
||||
onFieldChanged({
|
||||
instances,
|
||||
hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields,
|
||||
});
|
||||
}}
|
||||
disabled={getDisabled({ instances, field, mixedValues })}
|
||||
labelColor={
|
||||
isFieldHighlighted({ instances, field }) ? 'primary' : 'secondary'
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (field.valueType === 'multilinestring') {
|
||||
const { setValue } = field;
|
||||
const mixedValues = hasMixedValues({ instances, field });
|
||||
|
||||
+21
-5
@@ -65,16 +65,25 @@ export const CompactBehaviorPropertiesEditor = ({
|
||||
resourceManagementProps,
|
||||
}: CompactBehaviorPropertiesEditorProps): React.Node => {
|
||||
const [schemaRecomputeTrigger, forceRecomputeSchema] = useForceRecompute();
|
||||
// The schema is built from the first behavior. There is always one, but
|
||||
// stay safe as an empty list would break the whole properties panel.
|
||||
const behavior = behaviors.length > 0 ? behaviors[0] : null;
|
||||
// The behavior is identified by its pointer, as a new wrapper object is
|
||||
// given at each render.
|
||||
const behaviorPtr = behavior ? behavior.ptr : null;
|
||||
|
||||
const propertiesSchema = React.useMemo(
|
||||
const propertiesSchema: Schema = React.useMemo(
|
||||
() => {
|
||||
if (schemaRecomputeTrigger) {
|
||||
// schemaRecomputeTrigger allows to invalidate the schema when required.
|
||||
}
|
||||
const behaviorMetadataProperties = behaviorMetadata.getProperties();
|
||||
if (!behavior) return [];
|
||||
return propertiesMapToSchema({
|
||||
properties: behaviorMetadataProperties,
|
||||
defaultValueProperties: behaviorMetadataProperties,
|
||||
// Use the behavior properties (and not the metadata ones) so that
|
||||
// properties adapting themselves to the current values (labels,
|
||||
// visibility...) are properly displayed.
|
||||
properties: behavior.getProperties(),
|
||||
defaultValueProperties: behaviorMetadata.getProperties(),
|
||||
getPropertyValue: (instance, name) =>
|
||||
instance
|
||||
.getProperties()
|
||||
@@ -89,7 +98,14 @@ export const CompactBehaviorPropertiesEditor = ({
|
||||
shouldDisabledFieldsWithMixedValues: true,
|
||||
});
|
||||
},
|
||||
[schemaRecomputeTrigger, behaviorMetadata, object, layersContainer]
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
schemaRecomputeTrigger,
|
||||
behaviorPtr,
|
||||
behaviorMetadata,
|
||||
object,
|
||||
layersContainer,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -92,6 +92,15 @@ export type PrimitiveValueField =
|
||||
setValue: (instance: Instance, newValue: string) => void,
|
||||
...ValueFieldCommonProperties,
|
||||
|}
|
||||
| {|
|
||||
/** A number of which each bit is displayed as a toggle. */
|
||||
valueType: 'bitmask',
|
||||
getValue: Instance => number,
|
||||
setValue: (instance: Instance, newValue: number) => void,
|
||||
firstBit: number,
|
||||
bitCount: number,
|
||||
...ValueFieldCommonProperties,
|
||||
|}
|
||||
| {|
|
||||
valueType: 'multilinestring',
|
||||
getValue: Instance => string,
|
||||
|
||||
@@ -320,6 +320,34 @@ const createField = (
|
||||
isHighlighted: isHighlightedForString,
|
||||
disabled,
|
||||
};
|
||||
} else if (valueType === 'bitmask') {
|
||||
// A bitmask is a number where each bit is a flag. The extra information
|
||||
// tells which bits are meaningful, for example: "bitCount=4", "firstBit=4".
|
||||
const extraInfo = property.getExtraInfo().toJSArray();
|
||||
const getExtraInfoNumber = (key: string, defaultValue: number): number => {
|
||||
const entry = extraInfo.find(info => info.startsWith(`${key}=`));
|
||||
if (!entry) return defaultValue;
|
||||
const parsedValue = parseInt(entry.substring(key.length + 1), 10);
|
||||
return Number.isFinite(parsedValue) ? parsedValue : defaultValue;
|
||||
};
|
||||
|
||||
return {
|
||||
name,
|
||||
valueType: 'bitmask',
|
||||
getValue: getValueForNumber,
|
||||
setValue: (instance: Instance, newValue: number) => {
|
||||
setNumberValue(instance, name, newValue);
|
||||
},
|
||||
firstBit: getExtraInfoNumber('firstBit', 0),
|
||||
bitCount: getExtraInfoNumber('bitCount', 8),
|
||||
defaultValue: defaultValueNumber,
|
||||
getLabel,
|
||||
getDescription,
|
||||
hasImpactOnAllOtherFields: property.hasImpactOnOtherProperties(),
|
||||
visibility,
|
||||
isHighlighted: isHighlightedForNumber,
|
||||
disabled,
|
||||
};
|
||||
} else if (valueType === 'multilinestring') {
|
||||
return {
|
||||
name,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// @flow
|
||||
import propertiesMapToSchema from './PropertiesMapToSchema';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
describe('propertiesMapToSchema (bitmask properties)', () => {
|
||||
const makeSchema = (
|
||||
setUpProperties: (properties: gdMapStringPropertyDescriptor) => void
|
||||
) => {
|
||||
const properties = new gd.MapStringPropertyDescriptor();
|
||||
setUpProperties(properties);
|
||||
|
||||
const values = new Map<string, string>();
|
||||
const schema = propertiesMapToSchema({
|
||||
properties,
|
||||
defaultValueProperties: properties,
|
||||
getPropertyValue: (instance, name) => values.get(name) || '0',
|
||||
onUpdateProperty: (instance, name, value) => {
|
||||
values.set(name, value);
|
||||
},
|
||||
object: null,
|
||||
layersContainer: null,
|
||||
shouldDisabledFieldsWithMixedValues: false,
|
||||
});
|
||||
properties.delete();
|
||||
return schema;
|
||||
};
|
||||
|
||||
it('reads the edited bits from the extra information', () => {
|
||||
const schema = makeSchema(properties => {
|
||||
properties
|
||||
.getOrCreate('layers')
|
||||
.setValue('16')
|
||||
.setType('Bitmask')
|
||||
.addExtraInfo('bitCount=4')
|
||||
.addExtraInfo('firstBit=4')
|
||||
.setLabel('Layers');
|
||||
});
|
||||
|
||||
expect(schema).toHaveLength(1);
|
||||
const field = schema[0];
|
||||
expect(field.valueType).toBe('bitmask');
|
||||
expect(field.firstBit).toBe(4);
|
||||
expect(field.bitCount).toBe(4);
|
||||
});
|
||||
|
||||
it('falls back to the whole first byte without extra information', () => {
|
||||
const schema = makeSchema(properties => {
|
||||
properties
|
||||
.getOrCreate('masks')
|
||||
.setValue('255')
|
||||
.setType('Bitmask')
|
||||
.setLabel('Masks');
|
||||
});
|
||||
|
||||
const field = schema[0];
|
||||
expect(field.firstBit).toBe(0);
|
||||
expect(field.bitCount).toBe(8);
|
||||
});
|
||||
|
||||
it('leaves out a hidden bitmask, like any other hidden property', () => {
|
||||
const schema = makeSchema(properties => {
|
||||
properties
|
||||
.getOrCreate('masks')
|
||||
.setValue('255')
|
||||
.setType('Bitmask')
|
||||
.setLabel('Masks')
|
||||
.setHidden(true);
|
||||
});
|
||||
|
||||
expect(schema).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import Subheader from '../UI/Subheader';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import ColorField from '../UI/ColorField';
|
||||
import { CompactBitmaskField } from '../UI/CompactBitmaskField';
|
||||
import { MarkdownText } from '../UI/MarkdownText';
|
||||
import { rgbOrHexToRGBString } from '../Utils/ColorTransformer';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
@@ -307,6 +308,24 @@ const PropertiesEditor = ({
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
} else if (field.valueType === 'bitmask') {
|
||||
const { setValue, firstBit, bitCount } = field;
|
||||
return (
|
||||
<Column key={field.name} expand noMargin>
|
||||
<CompactBitmaskField
|
||||
id={field.name}
|
||||
label={getFieldLabel({ instances, field })}
|
||||
markdownDescription={getFieldDescription(field)}
|
||||
value={getFieldValue({ instances, field })}
|
||||
firstBit={firstBit}
|
||||
bitCount={bitCount}
|
||||
onChange={newValue => {
|
||||
instances.forEach(i => setValue(i, newValue));
|
||||
_onInstancesModified(instances);
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
} else if (field.valueType === 'multilinestring') {
|
||||
const { setValue } = field;
|
||||
return (
|
||||
|
||||
+9
-4
@@ -28,10 +28,12 @@ export const CompactBehaviorSharedDataPropertiesEditor = ({
|
||||
if (schemaRecomputeTrigger) {
|
||||
// schemaRecomputeTrigger allows to invalidate the schema when required.
|
||||
}
|
||||
const behaviorMetadataSharedProperties = behaviorMetadata.getSharedProperties();
|
||||
return propertiesMapToSchema({
|
||||
properties: behaviorMetadataSharedProperties,
|
||||
defaultValueProperties: behaviorMetadataSharedProperties,
|
||||
// Use the shared data properties (and not the metadata ones) so that
|
||||
// properties adapting themselves to the current values (labels,
|
||||
// visibility...) are properly displayed.
|
||||
properties: behaviorSharedData.getProperties(),
|
||||
defaultValueProperties: behaviorMetadata.getSharedProperties(),
|
||||
getPropertyValue: (instance, name) =>
|
||||
instance
|
||||
.getProperties()
|
||||
@@ -46,7 +48,10 @@ export const CompactBehaviorSharedDataPropertiesEditor = ({
|
||||
shouldDisabledFieldsWithMixedValues: false,
|
||||
});
|
||||
},
|
||||
[schemaRecomputeTrigger, behaviorMetadata]
|
||||
// The shared data is identified by its pointer, as a new wrapper object is
|
||||
// given at each render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[schemaRecomputeTrigger, behaviorSharedData.ptr, behaviorMetadata]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bits {
|
||||
display: grid;
|
||||
/* The number of columns is given by the component, so that bits are laid
|
||||
out on rows of at most 8, like they are usually read. */
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--theme-text-field-default-background-color);
|
||||
/* Keep the toggles at the same size whatever the number of bits, but let
|
||||
them shrink rather than overflow in a narrow panel. */
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.bit {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
min-width: 0px;
|
||||
border-radius: 3px;
|
||||
background-color: transparent;
|
||||
color: var(--theme-text-default-color);
|
||||
font-family: var(--gdevelop-modern-font-family);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s linear, color 0.1s linear;
|
||||
}
|
||||
|
||||
.bit:hover {
|
||||
background-color: var(--theme-hover-background-color);
|
||||
}
|
||||
|
||||
.bit.enabled {
|
||||
background-color: var(--theme-primary-color);
|
||||
color: var(--theme-primary-text-contrast-color);
|
||||
}
|
||||
|
||||
.bit:focus-visible {
|
||||
outline: 1px solid var(--theme-text-field-active-border-color);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.container.disabled .bit {
|
||||
color: var(--theme-text-field-disabled-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.container.disabled .bit:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.container.disabled .bit.enabled {
|
||||
background-color: var(--theme-text-field-disabled-color);
|
||||
color: var(--theme-text-field-default-background-color);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import classes from './CompactBitmaskField.module.css';
|
||||
import Text from '../Text';
|
||||
import { MarkdownText } from '../MarkdownText';
|
||||
import { tooltipEnterDelay } from '../Tooltip';
|
||||
|
||||
const styles = {
|
||||
label: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: '17px',
|
||||
maxHeight: 34, // 2 * lineHeight to limit to 2 lines.
|
||||
},
|
||||
};
|
||||
|
||||
export const isBitEnabled = (bitsValue: number, pos: number): boolean =>
|
||||
!!(bitsValue & (1 << pos));
|
||||
|
||||
export const enableBit = (
|
||||
bitsValue: number,
|
||||
pos: number,
|
||||
enable: boolean
|
||||
): number => (enable ? bitsValue | (1 << pos) : bitsValue & ~(1 << pos));
|
||||
|
||||
export type CompactBitmaskFieldProps = {|
|
||||
label?: string,
|
||||
markdownDescription?: ?string,
|
||||
/** The whole bitmask value. Bits outside of the edited ones are kept as is. */
|
||||
value: number,
|
||||
onChange: (newValue: number) => void,
|
||||
/** Index of the first edited bit (0 by default). */
|
||||
firstBit?: number,
|
||||
/** Number of bits to edit, starting at `firstBit`. */
|
||||
bitCount: number,
|
||||
id?: string,
|
||||
disabled?: boolean,
|
||||
labelColor?: 'primary' | 'secondary',
|
||||
|};
|
||||
|
||||
/**
|
||||
* Edit the bits of a number, displayed as a grid of numbered toggles.
|
||||
* Bits are labelled from 1, so that they match the layer numbers used
|
||||
* in events.
|
||||
*/
|
||||
export const CompactBitmaskField = ({
|
||||
label,
|
||||
markdownDescription,
|
||||
value,
|
||||
onChange,
|
||||
firstBit = 0,
|
||||
bitCount,
|
||||
id,
|
||||
disabled,
|
||||
labelColor,
|
||||
}: CompactBitmaskFieldProps): React.Node => {
|
||||
const title = !markdownDescription
|
||||
? label
|
||||
: [
|
||||
label,
|
||||
' - ',
|
||||
<MarkdownText key="markdown-desc" source={markdownDescription} />,
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
[classes.container]: true,
|
||||
[classes.disabled]: disabled,
|
||||
})}
|
||||
>
|
||||
{label && (
|
||||
<Tooltip
|
||||
title={title}
|
||||
enterDelay={tooltipEnterDelay}
|
||||
placement="bottom"
|
||||
>
|
||||
<Text
|
||||
noMargin
|
||||
// $FlowFixMe[incompatible-type]
|
||||
style={styles.label}
|
||||
color={labelColor === 'primary' ? 'primary' : 'secondary'}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div
|
||||
className={classes.bits}
|
||||
id={id}
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${Math.min(
|
||||
bitCount,
|
||||
8
|
||||
)}, minmax(0, 24px))`,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: bitCount }, (_, index) => {
|
||||
const bit = firstBit + index;
|
||||
const enabled = isBitEnabled(value, bit);
|
||||
return (
|
||||
<button
|
||||
key={bit}
|
||||
type="button"
|
||||
className={classNames({
|
||||
[classes.bit]: true,
|
||||
[classes.enabled]: enabled,
|
||||
})}
|
||||
aria-pressed={enabled}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(enableBit(value, bit, !enabled))}
|
||||
>
|
||||
{bit + 1}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompactBitmaskField;
|
||||
@@ -0,0 +1,61 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
|
||||
import muiDecorator from '../../ThemeDecorator';
|
||||
import paperDecorator from '../../PaperDecorator';
|
||||
|
||||
import { CompactBitmaskField } from '../../../UI/CompactBitmaskField';
|
||||
import { ColumnStackLayout } from '../../../UI/Layout';
|
||||
import Text from '../../../UI/Text';
|
||||
|
||||
export default {
|
||||
title: 'UI Building Blocks/CompactBitmaskField',
|
||||
component: CompactBitmaskField,
|
||||
decorators: [paperDecorator, muiDecorator],
|
||||
};
|
||||
|
||||
export const Default = (): React.Node => {
|
||||
const [layers, setLayers] = React.useState<number>(0b101);
|
||||
const [masks, setMasks] = React.useState<number>(0xffff);
|
||||
const [movingLayers, setMovingLayers] = React.useState<number>(0b00110000);
|
||||
return (
|
||||
<ColumnStackLayout expand>
|
||||
<Text size="sub-title">16 bits (Physics2 layers)</Text>
|
||||
<CompactBitmaskField
|
||||
label="Layers"
|
||||
markdownDescription="Layers the object belongs to, as a bitmask."
|
||||
value={layers}
|
||||
bitCount={16}
|
||||
onChange={setLayers}
|
||||
id="layers"
|
||||
/>
|
||||
<Text size="sub-title">16 bits, all enabled</Text>
|
||||
<CompactBitmaskField
|
||||
label="Masks"
|
||||
value={masks}
|
||||
bitCount={16}
|
||||
onChange={setMasks}
|
||||
/>
|
||||
<Text size="sub-title">
|
||||
4 bits starting at the 5th one (Physics3D layers of a moving object)
|
||||
</Text>
|
||||
<CompactBitmaskField
|
||||
label="Layers"
|
||||
value={movingLayers}
|
||||
firstBit={4}
|
||||
bitCount={4}
|
||||
onChange={setMovingLayers}
|
||||
/>
|
||||
<Text size="sub-title">Disabled</Text>
|
||||
<CompactBitmaskField
|
||||
label="Layers"
|
||||
value={0b1010}
|
||||
bitCount={8}
|
||||
onChange={() => {}}
|
||||
disabled
|
||||
/>
|
||||
<Text size="sub-title">Without label</Text>
|
||||
<CompactBitmaskField value={layers} bitCount={8} onChange={setLayers} />
|
||||
</ColumnStackLayout>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user