[WIP] Add CollisionMasksEditor in newIDE (preview not yet done)

This commit is contained in:
Florian Rival
2018-03-30 18:06:21 +02:00
parent 58ed74e020
commit 54d7d284c8
18 changed files with 1002 additions and 127 deletions

View File

@@ -24,9 +24,9 @@
"__WXMAC__",
"__WXOSX__",
"__WXOSX_COCOA__",
"GD_CORE_API=\/* Macro used to export classes on Windows, please ignore *\/",
"GD_API=\/* Macro used to export classes on Windows, please ignore *\/",
"GD_EXTENSION_API=\/* Macro used to export classes on Windows, please ignore *\/",
"GD_CORE_API=/* Macro used to export classes on Windows, please ignore */",
"GD_API=/* Macro used to export classes on Windows, please ignore */",
"GD_EXTENSION_API=/* Macro used to export classes on Windows, please ignore */",
"WXUSINGDLL"
],
"intelliSenseMode": "clang-x64",
@@ -45,7 +45,10 @@
"macFrameworkPath": [
"/System/Library/Frameworks",
"/Library/Frameworks"
]
],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17"
},
{
"name": "Linux",
@@ -66,9 +69,9 @@
"__WXMAC__",
"__WXOSX__",
"__WXOSX_COCOA__",
"GD_CORE_API=\/* Macro used to export classes on Windows, please ignore *\/",
"GD_API=\/* Macro used to export classes on Windows, please ignore *\/",
"GD_EXTENSION_API=\/* Macro used to export classes on Windows, please ignore *\/",
"GD_CORE_API=/* Macro used to export classes on Windows, please ignore */",
"GD_API=/* Macro used to export classes on Windows, please ignore */",
"GD_EXTENSION_API=/* Macro used to export classes on Windows, please ignore */",
"WXUSINGDLL"
],
"intelliSenseMode": "clang-x64",
@@ -102,9 +105,9 @@
"__WXMAC__",
"__WXOSX__",
"__WXOSX_COCOA__",
"GD_CORE_API=\/* Macro used to export classes on Windows, please ignore *\/",
"GD_API=\/* Macro used to export classes on Windows, please ignore *\/",
"GD_EXTENSION_API=\/* Macro used to export classes on Windows, please ignore *\/",
"GD_CORE_API=/* Macro used to export classes on Windows, please ignore */",
"GD_API=/* Macro used to export classes on Windows, please ignore */",
"GD_EXTENSION_API=/* Macro used to export classes on Windows, please ignore */",
"WXUSINGDLL"
],
"intelliSenseMode": "msvc-x64",
@@ -119,4 +122,4 @@
}
],
"version": 3
}
}

View File

@@ -26,14 +26,24 @@ public:
mutable std::vector<sf::Vector2f> edges; ///< Edges. Can be computed from vertices using ComputeEdges()
/**
* Moves each vertices from the given amount.
* \brief Get the vertices composing the polygon.
*/
std::vector<sf::Vector2f> & GetVertices() { return vertices; }
/**
* \brief Get the vertices composing the polygon.
*/
const std::vector<sf::Vector2f> & GetVertices() const { return vertices; }
/**
* \brief Moves each vertices from the given amount.
*
* \note Edges are updated, there is no need to call ComputeEdges after calling Move.
*/
void Move(float x, float y);
/**
* Rotate the polygon.
* \brief Rotate the polygon.
* \param angle Angle in radians
*
* \warning Rotation is made clockwise
@@ -42,18 +52,18 @@ public:
void Rotate(float angle);
/**
* Automatically fill edges vector using vertices.
* \brief Automatically fill edges vector using vertices.
*/
void ComputeEdges() const;
/**
* Check if the polygon is convex.
* \brief Check if the polygon is convex.
* \return true if the polygon is convex
*/
bool IsConvex() const;
/**
* Return the position of the center of the polygon
* \brief Return the position of the center of the polygon
*/
sf::Vector2f ComputeCenter() const;
@@ -62,7 +72,7 @@ public:
*/
///@{
/**
* Create a rectangle
* \brief Create a rectangle
*/
static Polygon2d CreateRectangle(float width, float height);
///@}

View File

@@ -56,6 +56,16 @@ public:
*/
std::vector<Polygon2d> GetCollisionMask() const;
/**
* \brief Get the custom collision mask.
*/
std::vector<Polygon2d> & GetCustomCollisionMask() { return customCollisionMask; };
/**
* \brief Get the custom collision mask.
*/
const std::vector<Polygon2d> & GetCustomCollisionMask() const { return customCollisionMask; };
/**
* \brief Set the custom collision mask.
* Call then `SetCollisionMaskAutomatic(false)` to use it.

View File

@@ -24,5 +24,7 @@ declare type gdParameterMetadata = EmscriptenObject;
declare type gdVariable = EmscriptenObject;
declare type gdVariablesContainer = EmscriptenObject;
declare type gdVectorPolygon2d = EmscriptenObject;
//Represents all objects that have serializeTo and unserializeFrom methods.
declare type gdSerializable = EmscriptenObject;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { TableRow, TableRowColumn } from 'material-ui/Table';
import Add from 'material-ui/svg-icons/content/add';
import IconButton from 'material-ui/IconButton';
import styles from './styles';
const AddPolygonRow = ({ onAdd }) => (
<TableRow>
<TableRowColumn style={styles.handleColumn} />
<TableRowColumn />
<TableRowColumn style={styles.coordinateColumn} />
<TableRowColumn style={styles.coordinateColumn} />
<TableRowColumn style={styles.toolColumn}>
<IconButton onClick={onAdd}>
<Add />
</IconButton>
</TableRowColumn>
</TableRow>
);
export default AddPolygonRow;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { TableRow, TableRowColumn } from 'material-ui/Table';
import AddCircle from 'material-ui/svg-icons/content/add-circle';
import IconButton from 'material-ui/IconButton';
import styles from './styles';
const AddVerticeRow = ({ onAdd }) => (
<TableRow>
<TableRowColumn style={styles.handleColumn} />
<TableRowColumn />
<TableRowColumn style={styles.coordinateColumn} />
<TableRowColumn style={styles.coordinateColumn} />
<TableRowColumn style={styles.toolColumn}>
<IconButton onClick={onAdd}>
<AddCircle />
</IconButton>
</TableRowColumn>
</TableRow>
);
export default AddVerticeRow;

View File

@@ -0,0 +1,40 @@
// @flow
import React from 'react';
import { mapVector } from '../../../../Utils/MapFor';
const styles = {
container: {
position: 'relative',
},
};
type Props = {|
polygons: gdVectorPolygon2d,
|};
export default class CollisionMasksPreview extends React.Component<
Props,
void
> {
render() {
const { polygons } = this.props;
return (
<div style={styles.container}>
<svg>
{mapVector(polygons, polygon => {
const vertices = polygon.getVertices();
return mapVector(vertices, vertex => (
<circle
cx={vertex.get_x()}
cy={vertex.get_y()}
r="3"
fill="blue"
/>
));
})}
</svg>
</div>
);
}
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { TableRow, TableRowColumn } from 'material-ui/Table';
import IconButton from 'material-ui/IconButton';
import Delete from 'material-ui/svg-icons/action/delete';
import muiThemeable from 'material-ui/styles/muiThemeable';
import styles from './styles';
const ThemablePolygonRow = ({ onRemove, isConvex, verticesCount, muiTheme }) => {
return (
<TableRow
style={{
backgroundColor: muiTheme.list.itemsBackgroundColor,
}}
>
<TableRowColumn style={styles.handleColumn}>
{/* <DragHandle /> Reordering polygons is not supported for now */}
</TableRowColumn>
{isConvex && (
<TableRowColumn>
{verticesCount === 3 && `Triangle`}
{verticesCount === 4 && `Quadrilateral`}
{verticesCount >= 5 && `Polygon with ${verticesCount} vertices`}
</TableRowColumn>
)}
{!isConvex && <TableRowColumn>Polygon is not convex!</TableRowColumn>}
<TableRowColumn style={styles.coordinateColumn} />
<TableRowColumn style={styles.coordinateColumn} />
<TableRowColumn style={styles.toolColumn}>
{!!onRemove && (
<IconButton onClick={onRemove}>
<Delete />
</IconButton>
)}
</TableRowColumn>
</TableRow>
);
};
const PointRow = muiThemeable()(ThemablePolygonRow);
export default PointRow;

View File

@@ -0,0 +1,167 @@
// @flow
import React, { Component } from 'react';
import flatten from 'lodash/flatten';
import {
Table,
TableBody,
TableHeader,
TableHeaderColumn,
TableRow,
TableRowColumn,
} from 'material-ui/Table';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
import { mapVector } from '../../../../Utils/MapFor';
import styles from './styles';
import VerticeRow from './VerticeRow';
import PolygonRow from './PolygonRow';
import AddVerticeRow from './AddVerticeRow';
import AddPolygonRow from './AddPolygonRow';
const gd = global.gd;
const SortableAddPolygonRow = SortableElement(AddPolygonRow);
const SortableAddVerticeRow = SortableElement(AddVerticeRow);
const SortableVerticeRow = SortableElement(VerticeRow);
const SortablePolygonRow = SortableElement(PolygonRow);
type PolygonsListBodyProps = {|
polygons: gdVectorPolygon2d,
onPolygonsUpdated: () => void,
|};
class PolygonsListBody extends Component<PolygonsListBodyProps, void> {
_onPolygonUpdated() {
this.forceUpdate();
this.props.onPolygonsUpdated();
}
updateVerticeX = (vertice, newValue) => {
vertice.set_x(newValue);
this._onPolygonUpdated();
};
updateVerticeY = (vertice, newValue) => {
vertice.set_y(newValue);
this._onPolygonUpdated();
};
render() {
const { polygons } = this.props;
const polygonRows = flatten(
mapVector(polygons, (polygon, i) => {
const vertices = polygon.getVertices();
const isConvex = polygon.isConvex();
return [
<SortablePolygonRow
index={i}
disabled
key={'polygon-' + i}
polygon={polygon}
onRemove={() => {
gd.removeFromVectorPolygon2d(polygons, i);
this._onPolygonUpdated();
}}
isConvex={isConvex}
verticesCount={vertices.size()}
/>,
mapVector(vertices, (vertice, j) => (
<SortableVerticeRow
index={i}
disabled
key={`polygon-${i}-vertice-${j}`}
verticeX={vertice.get_x()}
verticeY={vertice.get_y()}
onChangeVerticeX={newValue =>
this.updateVerticeX(vertice, newValue)}
onChangeVerticeY={newValue =>
this.updateVerticeY(vertice, newValue)}
onRemove={() => {
gd.removeFromVectorVector2f(polygon.getVertices(), j);
this._onPolygonUpdated();
}}
canRemove={vertices.size() > 3}
hasWarning={!isConvex}
/>
)),
<SortableAddVerticeRow
index={0}
key={`polygon-${i}-add-vertice-row`}
disabled
onAdd={() => {
const newVertice = new gd.Vector2f();
polygon.getVertices().push_back(newVertice);
newVertice.delete();
this._onPolygonUpdated();
}}
/>,
];
})
);
const addRow = (
<SortableAddPolygonRow
index={0}
key={'add-polygon-row'}
disabled
onAdd={() => {
const newPolygon = gd.Polygon2d.createRectangle(32, 32);
polygons.push_back(newPolygon);
this._onPolygonUpdated();
}}
/>
);
return (
<TableBody
displayRowCheckbox={false}
deselectOnClickaway={true}
showRowHover={true}
>
{[...polygonRows, addRow]}
</TableBody>
);
}
}
const SortablePolygonsListBody = SortableContainer(PolygonsListBody);
SortablePolygonsListBody.muiName = 'TableBody';
type Props = {|
polygons: gdVectorPolygon2d,
onPolygonsUpdated: () => void,
|};
export default class PolygonsList extends Component<Props, void> {
render() {
return (
<Table selectable={false}>
<TableHeader displaySelectAll={false} adjustForCheckbox={false}>
<TableRow>
<TableHeaderColumn style={styles.handleColumn} />
<TableHeaderColumn>Polygon</TableHeaderColumn>
<TableHeaderColumn style={styles.coordinateColumn}>
X
</TableHeaderColumn>
<TableHeaderColumn style={styles.coordinateColumn}>
Y
</TableHeaderColumn>
<TableRowColumn style={styles.toolColumn} />
</TableRow>
</TableHeader>
<SortablePolygonsListBody
polygons={this.props.polygons}
onPolygonsUpdated={this.props.onPolygonsUpdated}
onSortEnd={({ oldIndex, newIndex }) => {
// Reordering polygons is not supported for now
}}
helperClass="sortable-helper"
useDragHandle
lockToContainerEdges
/>
</Table>
);
}
}

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { TableRow, TableRowColumn } from 'material-ui/Table';
import IconButton from 'material-ui/IconButton';
import Delete from 'material-ui/svg-icons/action/delete';
import TextField from 'material-ui/TextField';
import Warning from 'material-ui/svg-icons/alert/warning';
import muiThemeable from 'material-ui/styles/muiThemeable';
import styles from './styles';
const ThemableVerticeRow = ({
hasWarning,
canRemove,
onRemove,
verticeX,
verticeY,
onChangeVerticeX,
onChangeVerticeY,
muiTheme,
}) => (
<TableRow style={{
backgroundColor: muiTheme.list.itemsBackgroundColor,
}}>
<TableRowColumn style={styles.handleColumn}>
{/* <DragHandle /> Reordering vertices is not supported for now */}
</TableRowColumn>
<TableRowColumn>
{hasWarning && <Warning />}
</TableRowColumn>
<TableRowColumn style={styles.coordinateColumn}>
<TextField
value={verticeX}
type="number"
id="vertice-x"
onChange={(e, value) => onChangeVerticeX(parseFloat(value || 0, 10))}
/>
</TableRowColumn>
<TableRowColumn style={styles.coordinateColumn}>
<TextField
value={verticeY}
type="number"
id="vertice-y"
onChange={(e, value) => onChangeVerticeY(parseFloat(value || 0, 10))}
/>
</TableRowColumn>
<TableRowColumn style={styles.toolColumn}>
{!!onRemove && (
<IconButton onClick={onRemove} disabled={!canRemove}>
<Delete />
</IconButton>
)}
</TableRowColumn>
</TableRow>
);
const PointRow = muiThemeable()(
ThemableVerticeRow
);
export default PointRow;

View File

@@ -0,0 +1,319 @@
import React, { Component } from 'react';
import SelectField from 'material-ui/SelectField';
import MenuItem from 'material-ui/MenuItem';
import FlatButton from 'material-ui/FlatButton';
import Toggle from 'material-ui/Toggle';
import EmptyMessage from '../../../../UI/EmptyMessage';
import { Line, Column } from '../../../../UI/Grid';
import { mapFor } from '../../../../Utils/MapFor';
import PolygonsList from './PolygonsList';
import CollisionMasksPreview from './CollisionMasksPreview';
import ImagePreview from '../../../ImagePreview';
import {
getCurrentElements,
allSpritesHaveSameCollisionMasksAs,
copyAnimationsSpriteCollisionMasks,
} from '../Utils/SpriteObjectHelper';
import every from 'lodash/every';
const gd = global.gd;
export default class CollisionMasksEditor extends Component {
state = {
animationIndex: 0,
directionIndex: 0,
spriteIndex: 0,
sameCollisionMasksForAnimations: true,
sameCollisionMasksForSprites: true,
};
componentDidMount() {
this._updateSameCollisionMasksToggles();
}
_updateCollisionMasks = () => {
const { object } = this.props;
const { animationIndex, directionIndex, spriteIndex } = this.state;
const spriteObject = gd.asSpriteObject(object);
const { animation, sprite } = getCurrentElements(
spriteObject,
animationIndex,
directionIndex,
spriteIndex
);
if (animation && sprite) {
if (this.state.sameCollisionMasksForAnimations) {
mapFor(0, spriteObject.getAnimationsCount(), i => {
const otherAnimation = spriteObject.getAnimation(i);
copyAnimationsSpriteCollisionMasks(sprite, otherAnimation);
});
} else if (this.state.sameCollisionMasksForSprites) {
copyAnimationsSpriteCollisionMasks(sprite, animation);
}
}
this.forceUpdate(); // Refresh the preview
if (this.props.onCollisionMasksUpdated)
this.props.onCollisionMasksUpdated();
};
chooseAnimation = index => {
this.setState(
{
animationIndex: index,
directionIndex: 0,
spriteIndex: 0,
},
() => this._updateSameCollisionMasksToggles()
);
};
chooseDirection = index => {
this.setState({
directionIndex: index,
spriteIndex: 0,
});
};
chooseSprite = index => {
this.setState({
spriteIndex: index,
});
};
_updateSameCollisionMasksToggles = () => {
const { object } = this.props;
const { animationIndex, directionIndex, spriteIndex } = this.state;
const spriteObject = gd.asSpriteObject(object);
const { animation, sprite } = getCurrentElements(
spriteObject,
animationIndex,
directionIndex,
spriteIndex
);
if (!animation || !sprite) return;
this.setState({
sameCollisionMasksForAnimations: every(
mapFor(0, spriteObject.getAnimationsCount(), i => {
const otherAnimation = spriteObject.getAnimation(i);
return allSpritesHaveSameCollisionMasksAs(sprite, otherAnimation);
})
),
sameCollisionMasksForSprites: allSpritesHaveSameCollisionMasksAs(
sprite,
animation
),
});
};
_onSetCollisionMaskAutomatic = (automatic: boolean = true) => {
const { object } = this.props;
const { animationIndex, directionIndex, spriteIndex } = this.state;
const spriteObject = gd.asSpriteObject(object);
const { sprite } = getCurrentElements(
spriteObject,
animationIndex,
directionIndex,
spriteIndex
);
if (!sprite) return;
sprite.setCollisionMaskAutomatic(automatic);
this._updateCollisionMasks();
};
_onToggleSamePointsForAnimation = enable => {
if (enable) {
// eslint-disable-next-line
const answer = confirm(
"Having the same collision masks for all animations will erase and reset all the other animations collision masks. This can't be undone. Are you sure you want to share these collision masks amongst all the animations of the object?"
);
if (!answer) return;
}
this.setState(
{
sameCollisionMasksForAnimations: enable,
sameCollisionMasksForSprites: enable
? true
: this.state.sameCollisionMasksForSprites,
},
() => {
this._updateCollisionMasks();
}
);
};
_onToggleSameCollisionMasksForSprites = enable => {
if (enable) {
// eslint-disable-next-line
const answer = confirm(
"Having the same collision masks for all frames will erase and reset all the other frames collision masks. This can't be undone. Are you sure you want to share these collision masks amongst all the frames of the animation?"
);
if (!answer) return;
}
this.setState(
{
sameCollisionMasksForAnimations: enable
? this.state.sameCollisionMasksForAnimations
: false,
sameCollisionMasksForSprites: enable,
},
() => {
this._updateCollisionMasks();
}
);
};
render() {
const { object, resourcesLoader, project } = this.props;
const {
sameCollisionMasksForAnimations,
sameCollisionMasksForSprites,
animationIndex,
directionIndex,
spriteIndex,
} = this.state;
const spriteObject = gd.asSpriteObject(object);
if (!object.getAnimationsCount()) return null;
const {
hasValidAnimation,
animation,
hasValidDirection,
direction,
hasValidSprite,
sprite,
} = getCurrentElements(
spriteObject,
animationIndex,
directionIndex,
spriteIndex
);
return (
<div>
<ImagePreview
resourceName={hasValidSprite ? sprite.getImageName() : ''}
resourcesLoader={resourcesLoader}
project={project}
>
{hasValidSprite && <CollisionMasksPreview polygons={sprite.getCustomCollisionMask()} />}
</ImagePreview>
<Line>
<Column expand>
<Toggle
label="Share same collision masks for all animations"
labelPosition="right"
toggled={sameCollisionMasksForAnimations}
onToggle={(e, checked) =>
this._onToggleSamePointsForAnimation(checked)}
/>
<Line>
{!sameCollisionMasksForAnimations && ( //TODO: factor with points editor?
<SelectField
floatingLabelText="Animation"
value={this.state.animationIndex}
onChange={(e, i, value) => this.chooseAnimation(value)}
>
{mapFor(0, spriteObject.getAnimationsCount(), i => {
const animation = spriteObject.getAnimation(i);
return (
<MenuItem
key={i}
value={i}
primaryText={`Animation #${i} ${animation.getName()}`}
/>
);
})}
</SelectField>
)}
{!sameCollisionMasksForAnimations &&
hasValidAnimation &&
animation.getDirectionsCount() > 1 && (
<SelectField
floatingLabelText="Direction"
value={this.state.directionIndex}
onChange={(e, i, value) => this.chooseDirection(value)}
>
{mapFor(0, animation.getDirectionsCount(), i => {
return (
<MenuItem
value={i}
key={i}
primaryText={`Direction #${i}`}
/>
);
})}
</SelectField>
)}
{!sameCollisionMasksForSprites &&
hasValidDirection && (
<SelectField
floatingLabelText="Frame"
value={this.state.spriteIndex}
onChange={(e, i, value) => this.chooseSprite(value)}
>
{mapFor(0, direction.getSpritesCount(), i => {
return (
<MenuItem
value={i}
key={i}
primaryText={`Frame #${i}`}
/>
);
})}
</SelectField>
)}
</Line>
<Toggle
label="Share same collision masks for all sprites of the animation"
labelPosition="right"
toggled={sameCollisionMasksForSprites}
onToggle={(e, checked) =>
this._onToggleSameCollisionMasksForSprites(checked)}
/>
</Column>
</Line>
{!!sprite &&
!sprite.isCollisionMaskAutomatic() && (
<React.Fragment>
<PolygonsList
polygons={sprite.getCustomCollisionMask()}
onPolygonsUpdated={this._updateCollisionMasks}
/>
<FlatButton
label="Restore the default collision mask"
primary={false}
onClick={() => this._onSetCollisionMaskAutomatic(true)}
/>
</React.Fragment>
)}
{!!sprite &&
sprite.isCollisionMaskAutomatic() && (
<React.Fragment>
<EmptyMessage>
This sprite uses the default collision mask, a rectangle that is
as large as the sprite.
</EmptyMessage>
<FlatButton
label="Use a custom collision mask"
primary={false}
onClick={() => this._onSetCollisionMaskAutomatic(false)}
/>
</React.Fragment>
)}
{!sprite && (
<EmptyMessage>
Choose an animation and frame to edit the collision masks
</EmptyMessage>
)}
</div>
);
}
}

View File

@@ -0,0 +1,14 @@
//TODO: Factor with styles.js from LayersList.
export default {
handleColumn: {
width: 24,
paddingLeft: 8,
paddingRight: 0,
},
coordinateColumn: {
width: 48,
},
toolColumn: {
width: 48,
},
};

View File

@@ -4,7 +4,7 @@ import Add from 'material-ui/svg-icons/content/add';
import IconButton from 'material-ui/IconButton';
import styles from './styles';
const AddLayerRow = ({ onAdd }) => (
const AddPointRow = ({ onAdd }) => (
<TableRow>
<TableRowColumn style={styles.handleColumn} />
<TableRowColumn />
@@ -18,4 +18,4 @@ const AddLayerRow = ({ onAdd }) => (
</TableRow>
);
export default AddLayerRow;
export default AddPointRow;

View File

@@ -19,12 +19,9 @@ const SortableAddPointRow = SortableElement(AddPointRow);
const SortablePointRow = SortableElement(PointRow);
class PointsListBody extends Component {
constructor() {
super();
this.state = {
nameErrors: {},
};
}
state = {
nameErrors: {},
};
_onPointsUpdated() {
this.forceUpdate();

View File

@@ -118,6 +118,85 @@ export const allSpritesHaveSamePointsAs = (originalSprite, animation) => {
);
};
export const copySpritePolygons = (originalSprite, destinationSprite) => {
if (originalSprite.ptr === destinationSprite.ptr) return;
destinationSprite.setCollisionMaskAutomatic(
originalSprite.isCollisionMaskAutomatic()
);
destinationSprite.getCustomCollisionMask().clear();
mapVector(originalSprite.getCustomCollisionMask(), originalPolygon => {
destinationSprite.getCustomCollisionMask().push_back(originalPolygon);
});
};
export const copyAnimationsSpriteCollisionMasks = (originalSprite, animation) => {
mapFor(0, animation.getDirectionsCount(), i => {
const direction = animation.getDirection(i);
mapFor(0, direction.getSpritesCount(), j => {
const sprite = direction.getSprite(j);
copySpritePolygons(originalSprite, sprite);
});
});
};
export const isSamePolygon = (polygon1, polygon2) => {
const polygon1Vertices = polygon1.getVertices();
const polygon2Vertices = polygon2.getVertices();
if (polygon1Vertices.size() !== polygon2Vertices.size()) return false;
return every(
mapVector(polygon1Vertices, (point1, index) => {
const point2 = polygon2Vertices.at(index);
return point1.get_x() === point2.get_x() && point1.get_y() === point2.get_y();
})
);
};
export const haveSameCollisionMasks = (sprite1, sprite2) => {
if (sprite1.isCollisionMaskAutomatic() !== sprite2.isCollisionMaskAutomatic())
return false;
if (
sprite1.isCollisionMaskAutomatic() &&
sprite2.isCollisionMaskAutomatic()
)
return true;
const sprite1CollisionMask = sprite1.getCustomCollisionMask();
const sprite2CollisionMask = sprite2.getCustomCollisionMask();
if (sprite1CollisionMask.size() !== sprite2CollisionMask.size()) return false;
return every(
mapVector(sprite1CollisionMask, (sprite1Polygon, index) => {
return isSamePolygon(sprite1Polygon, sprite2CollisionMask.at(index));
})
);
};
export const allSpritesHaveSameCollisionMasksAs = (
originalSprite,
animation
) => {
return every(
mapFor(0, animation.getDirectionsCount(), i => {
const direction = animation.getDirection(i);
return every(
mapFor(0, direction.getSpritesCount(), j => {
const sprite = direction.getSprite(j);
return haveSameCollisionMasks(sprite, originalSprite);
})
);
})
);
};
export const deleteSpritesFromAnimation = (animation, spritePtrs) => {
mapFor(0, animation.getDirectionsCount(), i => {
const direction = animation.getDirection(i);

View File

@@ -3,121 +3,179 @@ import {
allSpritesHaveSamePointsAs,
copyAnimationsSpritePoints,
deleteSpritesFromAnimation,
haveSameCollisionMasks,
} from './SpriteObjectHelper';
const gd = global.gd;
describe('History', () => {
it('can tell if two sprite have the exact same points', () => {
const sprite1 = new gd.Sprite();
const sprite2 = new gd.Sprite();
describe('SpriteObjectHelper', () => {
describe('Points related methods', () => {
it('can tell if two sprite have the exact same points', () => {
const sprite1 = new gd.Sprite();
const sprite2 = new gd.Sprite();
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
sprite1.getOrigin().setX(40);
expect(haveSamePoints(sprite1, sprite2)).toBe(false);
expect(haveSamePoints(sprite2, sprite1)).toBe(false);
sprite2.getOrigin().setX(40);
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
sprite1.getOrigin().setX(40);
expect(haveSamePoints(sprite1, sprite2)).toBe(false);
expect(haveSamePoints(sprite2, sprite1)).toBe(false);
sprite2.getOrigin().setX(40);
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
sprite1.setDefaultCenterPoint(false);
expect(haveSamePoints(sprite1, sprite2)).toBe(false);
expect(haveSamePoints(sprite2, sprite1)).toBe(false);
sprite2.setDefaultCenterPoint(false);
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
sprite1.setDefaultCenterPoint(false);
expect(haveSamePoints(sprite1, sprite2)).toBe(false);
expect(haveSamePoints(sprite2, sprite1)).toBe(false);
sprite2.setDefaultCenterPoint(false);
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
const customPoint1 = new gd.Point('CustomPoint');
sprite1.addPoint(customPoint1);
customPoint1.delete();
expect(haveSamePoints(sprite1, sprite2)).toBe(false);
expect(haveSamePoints(sprite2, sprite1)).toBe(false);
const customPoint2 = new gd.Point('CustomPoint');
sprite2.addPoint(customPoint2);
customPoint2.delete();
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
const customPoint1 = new gd.Point('CustomPoint');
sprite1.addPoint(customPoint1);
customPoint1.delete();
expect(haveSamePoints(sprite1, sprite2)).toBe(false);
expect(haveSamePoints(sprite2, sprite1)).toBe(false);
const customPoint2 = new gd.Point('CustomPoint');
sprite2.addPoint(customPoint2);
customPoint2.delete();
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
sprite1.getPoint('CustomPoint').setY(10);
expect(haveSamePoints(sprite1, sprite2)).toBe(false);
expect(haveSamePoints(sprite2, sprite1)).toBe(false);
sprite2.getPoint('CustomPoint').setY(10);
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
});
sprite1.getPoint('CustomPoint').setY(10);
expect(haveSamePoints(sprite1, sprite2)).toBe(false);
expect(haveSamePoints(sprite2, sprite1)).toBe(false);
sprite2.getPoint('CustomPoint').setY(10);
expect(haveSamePoints(sprite1, sprite2)).toBe(true);
expect(haveSamePoints(sprite2, sprite1)).toBe(true);
});
it('can tell if all sprites of animations have the exact same points', () => {
const originalSprite = new gd.Sprite();
it('can tell if all sprites of animations have the exact same points', () => {
const originalSprite = new gd.Sprite();
const animation1 = new gd.Animation();
animation1.setDirectionsCount(1);
const sprite1 = new gd.Sprite();
const sprite2 = new gd.Sprite();
const animation1 = new gd.Animation();
animation1.setDirectionsCount(1);
const sprite1 = new gd.Sprite();
const sprite2 = new gd.Sprite();
animation1.getDirection(0).addSprite(sprite1);
animation1.getDirection(0).addSprite(sprite2);
animation1.getDirection(0).addSprite(sprite1);
animation1.getDirection(0).addSprite(sprite2);
const animation2 = new gd.Animation();
animation2.setDirectionsCount(1);
const sprite3 = new gd.Sprite();
const sprite4 = new gd.Sprite();
sprite4.setDefaultCenterPoint(false);
sprite4.getCenter().setY(5);
const animation2 = new gd.Animation();
animation2.setDirectionsCount(1);
const sprite3 = new gd.Sprite();
const sprite4 = new gd.Sprite();
sprite4.setDefaultCenterPoint(false);
sprite4.getCenter().setY(5);
animation2.getDirection(0).addSprite(sprite3);
animation2.getDirection(0).addSprite(sprite4);
animation2.getDirection(0).addSprite(sprite3);
animation2.getDirection(0).addSprite(sprite4);
expect(allSpritesHaveSamePointsAs(originalSprite, animation1)).toBe(true);
expect(allSpritesHaveSamePointsAs(originalSprite, animation2)).toBe(false);
});
expect(allSpritesHaveSamePointsAs(originalSprite, animation1)).toBe(true);
expect(allSpritesHaveSamePointsAs(originalSprite, animation2)).toBe(
false
);
});
it('can copy points of a sprite in all sprites of an animation', () => {
const animation1 = new gd.Animation();
animation1.setDirectionsCount(1);
const emptySprite = new gd.Sprite();
const spriteWithCustomPoints = new gd.Sprite();
it('can copy points of a sprite in all sprites of an animation', () => {
const animation1 = new gd.Animation();
animation1.setDirectionsCount(1);
const emptySprite = new gd.Sprite();
const spriteWithCustomPoints = new gd.Sprite();
const point = new gd.Point('CustomPoint');
spriteWithCustomPoints.addPoint(point);
point.delete();
spriteWithCustomPoints.setDefaultCenterPoint(false);
spriteWithCustomPoints.getCenter().setY(5);
spriteWithCustomPoints.getPoint('CustomPoint').setX(1);
spriteWithCustomPoints.getPoint('CustomPoint').setY(2);
const point = new gd.Point('CustomPoint');
spriteWithCustomPoints.addPoint(point);
point.delete();
spriteWithCustomPoints.setDefaultCenterPoint(false);
spriteWithCustomPoints.getCenter().setY(5);
spriteWithCustomPoints.getPoint('CustomPoint').setX(1);
spriteWithCustomPoints.getPoint('CustomPoint').setY(2);
animation1.getDirection(0).addSprite(emptySprite);
animation1.getDirection(0).addSprite(spriteWithCustomPoints);
animation1.getDirection(0).addSprite(emptySprite);
animation1.getDirection(0).addSprite(emptySprite);
animation1.getDirection(0).addSprite(spriteWithCustomPoints);
animation1.getDirection(0).addSprite(emptySprite);
const animation2 = new gd.Animation();
animation2.getDirection(0).addSprite(emptySprite);
copyAnimationsSpritePoints(spriteWithCustomPoints, animation2);
expect(allSpritesHaveSamePointsAs(spriteWithCustomPoints, animation2)).toBe(
true
);
const animation2 = new gd.Animation();
animation2.getDirection(0).addSprite(emptySprite);
copyAnimationsSpritePoints(spriteWithCustomPoints, animation2);
expect(
allSpritesHaveSamePointsAs(spriteWithCustomPoints, animation2)
).toBe(true);
copyAnimationsSpritePoints(
animation1.getDirection(0).getSprite(1),
animation1
);
expect(
haveSamePoints(
animation1.getDirection(0).getSprite(0),
spriteWithCustomPoints
)
).toBe(true);
expect(
haveSamePoints(
copyAnimationsSpritePoints(
animation1.getDirection(0).getSprite(1),
spriteWithCustomPoints
)
).toBe(true);
expect(
haveSamePoints(
animation1.getDirection(0).getSprite(2),
spriteWithCustomPoints
)
).toBe(true);
animation1
);
expect(
haveSamePoints(
animation1.getDirection(0).getSprite(0),
spriteWithCustomPoints
)
).toBe(true);
expect(
haveSamePoints(
animation1.getDirection(0).getSprite(1),
spriteWithCustomPoints
)
).toBe(true);
expect(
haveSamePoints(
animation1.getDirection(0).getSprite(2),
spriteWithCustomPoints
)
).toBe(true);
});
});
describe('Collision masks related methods', () => {
it('can tell if two sprite have the exact same collision masks', () => {
const addVertice = (polygon, x, y) => {
const vertice = new gd.Vector2f();
vertice.x = x;
vertice.y = y;
polygon.getVertices().push_back(vertice);
vertice.delete();
};
const sprite1 = new gd.Sprite();
const sprite2 = new gd.Sprite();
expect(haveSameCollisionMasks(sprite1, sprite2)).toBe(true);
expect(haveSameCollisionMasks(sprite2, sprite1)).toBe(true);
sprite1.setCollisionMaskAutomatic(false);
expect(haveSameCollisionMasks(sprite1, sprite2)).toBe(false);
expect(haveSameCollisionMasks(sprite2, sprite1)).toBe(false);
{
const polygon1 = new gd.Polygon2d();
addVertice(polygon1, 0, 0);
addVertice(polygon1, 0, 10);
addVertice(polygon1, 10, 0);
sprite1.getCustomCollisionMask().push_back(polygon1);
polygon1.delete();
expect(haveSameCollisionMasks(sprite1, sprite2)).toBe(false);
expect(haveSameCollisionMasks(sprite2, sprite1)).toBe(false);
}
{
sprite2.setCollisionMaskAutomatic(false);
const polygon2 = new gd.Polygon2d();
addVertice(polygon2, 0, 0);
addVertice(polygon2, 0, 10);
addVertice(polygon2, 10, 0);
sprite2.getCustomCollisionMask().push_back(polygon2);
polygon2.delete();
expect(haveSameCollisionMasks(sprite1, sprite2)).toBe(true);
expect(haveSameCollisionMasks(sprite2, sprite1)).toBe(true);
sprite2.getCustomCollisionMask().at(0).getVertices().at(1).set_x(-20);
expect(haveSameCollisionMasks(sprite1, sprite2)).toBe(false);
expect(haveSameCollisionMasks(sprite2, sprite1)).toBe(false);
sprite1.getCustomCollisionMask().at(0).getVertices().at(1).set_x(-20);
expect(haveSameCollisionMasks(sprite1, sprite2)).toBe(true);
expect(haveSameCollisionMasks(sprite2, sprite1)).toBe(true);
}
});
});
it('can remove sprites using the sprites pointers', () => {

View File

@@ -17,6 +17,7 @@ import ContextMenu from '../../../UI/Menu/ContextMenu';
import { showWarningBox } from '../../../UI/Messages/MessageBox';
import ResourcesLoader from '../../../ResourcesLoader';
import PointsEditor from './PointsEditor';
import CollisionMasksEditor from './CollisionMasksEditor';
import { deleteSpritesFromAnimation } from './Utils/SpriteObjectHelper';
const gd = global.gd;
@@ -283,6 +284,7 @@ class AnimationsListContainer extends Component {
export default class SpriteEditor extends Component {
state = {
pointsEditorOpen: false,
collisionMasksEditorOpen: false,
};
constructor(props) {
@@ -297,10 +299,10 @@ export default class SpriteEditor extends Component {
});
};
openHitboxesEditor = (open = true) => {
alert(
"Hitboxes editor is not ready yet! We're working on it and it will be available soon."
);
openCollisionMasksEditor = (open = true) => {
this.setState({
collisionMasksEditorOpen: open,
});
};
render() {
@@ -327,7 +329,7 @@ export default class SpriteEditor extends Component {
<RaisedButton
label="Edit hitboxes"
primary={false}
onClick={() => this.openHitboxesEditor(true)}
onClick={() => this.openCollisionMasksEditor(true)}
disabled={spriteObject.getAnimationsCount() === 0}
/>
<RaisedButton
@@ -359,7 +361,31 @@ export default class SpriteEditor extends Component {
resourcesLoader={this.resourcesLoader}
project={project}
onPointsUpdated={() =>
this.forceUpdate() /*Force update to ensure dialog is properly positionned*/}
this.forceUpdate() /*Force update to ensure dialog is properly positioned*/}
/>
</Dialog>
)}
{this.state.collisionMasksEditorOpen && (
<Dialog
actions={
<FlatButton
label="Close"
primary
onClick={() => this.openCollisionMasksEditor(false)}
/>
}
autoScrollBodyContent
noMargin
modal
onRequestClose={() => this.openCollisionMasksEditor(false)}
open={this.state.collisionMasksEditorOpen}
>
<CollisionMasksEditor
object={spriteObject}
resourcesLoader={this.resourcesLoader}
project={project}
onCollisionMasksUpdated={() =>
this.forceUpdate() /*Force update to ensure dialog is properly positioned*/}
/>
</Dialog>
)}

View File

@@ -23,6 +23,7 @@ import TiledSpriteEditor from '../ObjectEditor/Editors/TiledSpriteEditor';
import PanelSpriteEditor from '../ObjectEditor/Editors/PanelSpriteEditor';
import SpriteEditor from '../ObjectEditor/Editors/SpriteEditor';
import PointsEditor from '../ObjectEditor/Editors/SpriteEditor/PointsEditor';
import CollisionMasksEditor from '../ObjectEditor/Editors/SpriteEditor/CollisionMasksEditor';
import EmptyEditor from '../ObjectEditor/Editors/EmptyEditor';
import ImageThumbnail from '../ObjectEditor/ImageThumbnail';
import ShapePainterEditor from '../ObjectEditor/Editors/ShapePainterEditor';
@@ -424,6 +425,15 @@ storiesOf('SpriteEditor and related editors', module)
resourcesLoader={ResourcesLoader}
/>
</SerializedObjectDisplay>
))
.add('CollisionMasksEditor', () => (
<SerializedObjectDisplay object={spriteObject}>
<CollisionMasksEditor
object={spriteObject}
project={project}
resourcesLoader={ResourcesLoader}
/>
</SerializedObjectDisplay>
));
storiesOf('ShapePainterEditor', module)