Merge remote-tracking branch 'origin/master'
| | |
| | | 'common.realname':'Real Name', |
| | | 'common.idcard':'ID Number', |
| | | 'common.introduction':'Introduction', |
| | | 'common.execute':'Execute', |
| | | '':'', |
| | | '':'', |
| | | '':'', |
| | |
| | | 'map.edit': 'Edit Model', |
| | | 'map.edit.close': 'Exit Edit', |
| | | 'map.device.add': 'Add New Device', |
| | | 'map.device.oper': 'Device Settings', |
| | | 'map.model.observer': 'Observer Pattern', |
| | | 'map.model.editor': 'Editor Pattern', |
| | | '': '', |
| | | '': '', |
| | | '': '', |
| | | '': '', |
| | | '': '', |
| | | '': '', |
| | | '': '', |
| | | 'map.settings.type': 'Type', |
| | | 'map.settings.position': 'Position', |
| | | 'map.settings.scale': 'Scale', |
| | | 'map.settings.rotation': 'Rotation', |
| | | 'map.settings.copy': 'Copy', |
| | | 'map.settings.left': 'Left', |
| | | 'map.settings.right': 'Right', |
| | | 'map.settings.top': 'Top', |
| | | 'map.settings.bottom': 'Bottom', |
| | | '': '', |
| | | '': '', |
| | | '': '', |
| | | '': '', |
| | | } |
| | |
| | | }); |
| | | |
| | | import agv from '/public/img/map/agv.svg' |
| | | import { set } from 'lodash'; |
| | | |
| | | const Device = (props) => { |
| | | const { styles } = useStyles(); |
| | | const [dragging, setDragging] = useState(false); |
| | | const [dragSprite, setDragSprite] = useState(null); |
| | | const [dragSpriteType, setDragSpriteType] = useState(null); |
| | | |
| | | const onDragStart = (e, type) => { |
| | | setDragging(true); |
| | | setDragSpriteType(type); |
| | | const sprite = PIXI.Sprite.from(agv); |
| | | setDragSprite(sprite); |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | const handleMouseMove = (e) => { |
| | | if (dragging) { |
| | | props.onDrop(dragSprite, e.clientX, e.clientY); |
| | | props.onDrop(dragSprite, dragSpriteType, e.clientX, e.clientY); |
| | | setDragging(false); |
| | | setDragSpriteType(null); |
| | | } |
| | | }; |
| | | window.addEventListener('mousemove', handleMouseMove); |
| | | return () => window.removeEventListener('mousemove', handleMouseMove); |
| | | }, [dragging, props.onDrop, props.onCancel]); |
| | | |
| | | const onDragStart = (e) => { |
| | | setDragging(true) |
| | | props.onCancel(); |
| | | const sprite = PIXI.Sprite.from(agv); |
| | | sprite.anchor.set(0.5); |
| | | setDragSprite(sprite); |
| | | }; |
| | | |
| | | return ( |
| | | <> |
| | |
| | | width='50px' |
| | | preview={false} |
| | | draggable="true" |
| | | onDragStart={onDragStart} |
| | | onDragStart={(e) => onDragStart(e, 'AGV')} |
| | | /> |
| | | <div>AGV</div> |
| | | </Col> |
| | |
| | | width='50px' |
| | | preview={false} |
| | | draggable="true" |
| | | onDragStart={onDragStart} |
| | | onDragStart={(e) => onDragStart(e, 'AGV')} |
| | | /> |
| | | <div>AGV</div> |
| | | </Col> |
| | |
| | | width='50px' |
| | | preview={false} |
| | | draggable="true" |
| | | onDragStart={onDragStart} |
| | | onDragStart={(e) => onDragStart(e, 'AGV')} |
| | | /> |
| | | <div>AGV</div> |
| | | </Col> |
| | |
| | | width='50px' |
| | | preview={false} |
| | | draggable="true" |
| | | onDragStart={onDragStart} |
| | | onDragStart={(e) => onDragStart(e, 'AGV')} |
| | | /> |
| | | <div>AGV</div> |
| | | </Col> |
| | |
| | | width='50px' |
| | | preview={false} |
| | | draggable="true" |
| | | onDragStart={onDragStart} |
| | | onDragStart={(e) => onDragStart(e, 'AGV')} |
| | | /> |
| | | <div>AGV</div> |
| | | </Col> |
| | |
| | | width='50px' |
| | | preview={false} |
| | | draggable="true" |
| | | onDragStart={onDragStart} |
| | | onDragStart={(e) => onDragStart(e, 'AGV')} |
| | | /> |
| | | <div>AGV</div> |
| | | </Col> |
| | |
| | | width='50px' |
| | | preview={false} |
| | | draggable="true" |
| | | onDragStart={onDragStart} |
| | | onDragStart={(e) => onDragStart(e, 'AGV')} |
| | | /> |
| | | <div>AGV</div> |
| | | </Col> |
| | |
| | | width='50px' |
| | | preview={false} |
| | | draggable="true" |
| | | onDragStart={onDragStart} |
| | | onDragStart={(e) => onDragStart(e, 'AGV')} |
| | | /> |
| | | <div>AGV</div> |
| | | </Col> |
| | |
| | | width='50px' |
| | | preview={false} |
| | | draggable="true" |
| | | onDragStart={onDragStart} |
| | | onDragStart={(e) => onDragStart(e, 'AGV')} |
| | | /> |
| | | <div>AGV</div> |
| | | </Col> |
New file |
| | |
| | | import React, { useState, useRef, useEffect } from 'react'; |
| | | import { Col, Form, Input, Row, Checkbox, Slider, Select, Drawer, Space, Button, InputNumber, Switch } from 'antd'; |
| | | import { FormattedMessage, useIntl, useModel } from '@umijs/max'; |
| | | import { createStyles } from 'antd-style'; |
| | | import './index.css'; |
| | | import * as Utils from '../utils' |
| | | import * as PIXI from 'pixi.js'; |
| | | import moment from 'moment'; |
| | | import Http from '@/utils/http'; |
| | | |
| | | const useStyles = createStyles(({ token, css }) => { |
| | | |
| | | }) |
| | | |
| | | const SpriteSettings = (props) => { |
| | | const intl = useIntl(); |
| | | const { styles } = useStyles(); |
| | | const { curSprite } = props; |
| | | const [form] = Form.useForm(); |
| | | |
| | | useEffect(() => { |
| | | |
| | | }, []); |
| | | |
| | | useEffect(() => { |
| | | form.resetFields(); |
| | | if (curSprite) { |
| | | form.setFieldsValue({ |
| | | x: curSprite.position.x, |
| | | y: curSprite.position.y, |
| | | scale: Math.max(curSprite.scale.x, curSprite.scale.y), |
| | | scaleSlider: Math.max(curSprite.scale.x, curSprite.scale.y), |
| | | rotation: curSprite.rotation * 180 / Math.PI, |
| | | rotationSlider: curSprite.rotation * 180 / Math.PI, |
| | | }) |
| | | } |
| | | }, [form, props]) |
| | | |
| | | const handleCancel = () => { |
| | | props.onCancel(); |
| | | }; |
| | | |
| | | const handleOk = () => { |
| | | form.submit(); |
| | | } |
| | | |
| | | const handleFinish = async (values) => { |
| | | props.onSubmit({ ...values }); |
| | | } |
| | | |
| | | const formValuesChange = (changeList) => { |
| | | if (changeList && changeList.length > 0) { |
| | | changeList.forEach(change => { |
| | | const { name: nameList, value } = change; |
| | | nameList.forEach(name => { |
| | | switch (name) { |
| | | case 'x': |
| | | curSprite.position.x = value; |
| | | break; |
| | | case 'y': |
| | | curSprite.position.x = value; |
| | | break; |
| | | case 'scaleSlider': |
| | | form.setFieldsValue({ |
| | | scale: value |
| | | }) |
| | | curSprite.scale.set(value); |
| | | break; |
| | | case 'scale': |
| | | form.setFieldsValue({ |
| | | scaleSlider: value |
| | | }) |
| | | curSprite.scale.set(value); |
| | | break; |
| | | case 'rotationSlider': |
| | | form.setFieldsValue({ |
| | | rotation: value |
| | | }) |
| | | curSprite.rotation = value * Math.PI / 180; |
| | | break; |
| | | case 'rotation': |
| | | form.setFieldsValue({ |
| | | rotationSlider: value |
| | | }) |
| | | curSprite.rotation = value * Math.PI / 180; |
| | | break; |
| | | default: |
| | | break; |
| | | } |
| | | Utils.removeSelectedEffect(); |
| | | Utils.showSelectedEffect(curSprite); |
| | | }) |
| | | }) |
| | | } |
| | | } |
| | | |
| | | const onFinishFailed = (errorInfo) => { |
| | | }; |
| | | |
| | | return ( |
| | | <> |
| | | <Drawer |
| | | open={props.open} |
| | | onClose={handleCancel} |
| | | getContainer={props.refCurr} |
| | | rootStyle={{ position: "absolute" }} |
| | | mask={false} |
| | | width={570} |
| | | extra={ |
| | | <Space> |
| | | <Button onClick={handleCancel}> |
| | | <FormattedMessage id='common.cancel' defaultMessage='取消' /> |
| | | </Button> |
| | | <Button onClick={handleOk} type="primary"> |
| | | <FormattedMessage id='common.submit' defaultMessage='保存' /> |
| | | </Button> |
| | | </Space> |
| | | } |
| | | > |
| | | <Form |
| | | form={form} |
| | | onFieldsChange={formValuesChange} |
| | | initialValues={{ |
| | | }} |
| | | onFinish={handleFinish} |
| | | onFinishFailed={onFinishFailed} |
| | | autoComplete="off" |
| | | style={{ |
| | | maxWidth: 600, |
| | | }} |
| | | size='default' // small | default | large |
| | | variant='filled' // outlined | borderless | filled |
| | | labelWrap // label 换行 |
| | | disabled={false} |
| | | layout='horizontal' |
| | | > |
| | | <Row gutter={[24, 16]}> |
| | | |
| | | {/* */} |
| | | <Col span={24}> |
| | | <Row gutter={24}> |
| | | <Col span={18}> |
| | | <Form.Item |
| | | label={intl.formatMessage({ id: 'map.settings.type', defaultMessage: '类型' })} |
| | | labelCol={{ span: 4 }} |
| | | > |
| | | <span className="ant-form-text">China</span> |
| | | </Form.Item> |
| | | </Col> |
| | | </Row> |
| | | </Col> |
| | | |
| | | {/* position */} |
| | | <Col span={24}> |
| | | <Row gutter={24}> |
| | | <Col span={18}> |
| | | <Form.Item |
| | | label={intl.formatMessage({ id: 'map.settings.position', defaultMessage: '坐标' })} |
| | | labelCol={{ span: 4 }} |
| | | > |
| | | <Space.Compact> |
| | | <Form.Item |
| | | name='x' |
| | | noStyle |
| | | rules={[ |
| | | { |
| | | required: true, |
| | | }, |
| | | ]} |
| | | > |
| | | <InputNumber |
| | | addonBefore={<Space.Compact>x</Space.Compact>} |
| | | style={{ |
| | | width: '50%', |
| | | }} |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | name='y' |
| | | noStyle |
| | | rules={[ |
| | | { |
| | | required: true, |
| | | }, |
| | | ]} |
| | | > |
| | | <InputNumber |
| | | addonBefore={<Space.Compact>y</Space.Compact>} |
| | | style={{ |
| | | width: '50%', |
| | | }} |
| | | /> |
| | | </Form.Item> |
| | | </Space.Compact> |
| | | </Form.Item> |
| | | </Col> |
| | | </Row> |
| | | </Col> |
| | | |
| | | {/* scale */} |
| | | <Col span={24}> |
| | | <Row gutter={24}> |
| | | <Col span={18}> |
| | | <Form.Item |
| | | label={intl.formatMessage({ id: 'map.settings.scale', defaultMessage: '缩放' })} |
| | | name="scaleSlider" |
| | | labelCol={{ span: 4 }} |
| | | > |
| | | <Slider |
| | | min={0.1} |
| | | max={10} |
| | | step={0.1} |
| | | marks={{ |
| | | 0.1: '0.1', |
| | | 1: '1', |
| | | 10: '10', |
| | | }} |
| | | /> |
| | | </Form.Item> |
| | | </Col> |
| | | <Col span={6}> |
| | | <Form.Item |
| | | name="scale" |
| | | labelCol={{ span: 4 }} |
| | | > |
| | | <InputNumber |
| | | changeOnWheel |
| | | min={0.1} max={10} defaultValue={1} step={0.1} |
| | | rules={[ |
| | | { |
| | | required: true, |
| | | }, |
| | | ]} |
| | | /> |
| | | </Form.Item> |
| | | </Col> |
| | | </Row> |
| | | </Col> |
| | | |
| | | {/* rotation */} |
| | | <Col span={24}> |
| | | <Row gutter={24}> |
| | | <Col span={18}> |
| | | <Form.Item |
| | | label={intl.formatMessage({ id: 'map.settings.rotation', defaultMessage: '角度' })} |
| | | name="rotationSlider" |
| | | labelCol={{ span: 4 }} |
| | | > |
| | | <Slider |
| | | min={0} |
| | | max={360} |
| | | step={1} |
| | | marks={{ |
| | | 0: '0°', |
| | | 90: '90°', |
| | | 180: '180°', |
| | | 270: '270°', |
| | | 360: '360°', |
| | | }} |
| | | /> |
| | | </Form.Item> |
| | | </Col> |
| | | <Col span={6}> |
| | | <Form.Item |
| | | name="rotation" |
| | | labelCol={{ span: 4 }} |
| | | > |
| | | <InputNumber |
| | | changeOnWheel |
| | | min={0} max={360} defaultValue={0} |
| | | rules={[ |
| | | { |
| | | required: true, |
| | | }, |
| | | ]} |
| | | /> |
| | | </Form.Item> |
| | | </Col> |
| | | </Row> |
| | | </Col> |
| | | |
| | | {/* 复制 */} |
| | | <Col span={24}> |
| | | <Row gutter={24}> |
| | | <Col span={18}> |
| | | <Form.Item |
| | | label={intl.formatMessage({ id: 'map.settings.copy', defaultMessage: '复制' })} |
| | | labelCol={{ span: 4 }} |
| | | > |
| | | <Space.Compact> |
| | | <Form.Item |
| | | name="copyDire" |
| | | > |
| | | <Select |
| | | defaultValue="left" |
| | | style={{ width: 80 }} |
| | | options={[ |
| | | { value: 'left', label: intl.formatMessage({ id: 'map.settings.left', defaultMessage: '左' }) }, |
| | | { value: 'right', label: intl.formatMessage({ id: 'map.settings.right', defaultMessage: '右' }) }, |
| | | { value: 'top', label: intl.formatMessage({ id: 'map.settings.top', defaultMessage: '上' }) }, |
| | | { value: 'bottom', label: intl.formatMessage({ id: 'map.settings.bottom', defaultMessage: '下' }) }, |
| | | ]} |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | name='copyCount' |
| | | noStyle |
| | | rules={[ |
| | | { |
| | | required: true, |
| | | }, |
| | | ]} |
| | | > |
| | | <InputNumber |
| | | addonBefore={<Space.Compact></Space.Compact>} |
| | | style={{ |
| | | width: '50%', |
| | | }} |
| | | min={1} defaultValue={1} step={1} |
| | | /> |
| | | </Form.Item> |
| | | <Form.Item> |
| | | <Button> |
| | | <FormattedMessage id='common.execute' defaultMessage='执行' /> |
| | | </Button> |
| | | </Form.Item> |
| | | </Space.Compact> |
| | | </Form.Item> |
| | | </Col> |
| | | </Row> |
| | | </Col> |
| | | |
| | | |
| | | {/* <Col span={12}> |
| | | <Form.Item |
| | | label="Username" |
| | | name="username" |
| | | hasFeedback |
| | | validateTrigger="onBlur" |
| | | validateDebounce={1000} |
| | | rules={[ |
| | | { |
| | | required: false, |
| | | } |
| | | ]} |
| | | > |
| | | <Input disabled={false} /> |
| | | </Form.Item> |
| | | </Col> |
| | | <Col span={24}> |
| | | <Form.Item |
| | | label="Switch" |
| | | valuePropName="checked" |
| | | > |
| | | <Switch /> |
| | | </Form.Item> |
| | | </Col> |
| | | <Col span={24}> |
| | | <Form.Item label="Memo"> |
| | | <Input.TextArea /> |
| | | </Form.Item> |
| | | </Col> |
| | | <Col span={24}> |
| | | <Form.Item label="Address"> |
| | | <Space.Compact> |
| | | <Form.Item |
| | | name={['address', 'province']} |
| | | noStyle |
| | | rules={[ |
| | | { |
| | | required: false, |
| | | message: 'Province is required', |
| | | }, |
| | | ]} |
| | | > |
| | | <Select placeholder="Select province"> |
| | | <Option value="Zhejiang">Zhejiang</Option> |
| | | <Option value="Jiangsu">Jiangsu</Option> |
| | | </Select> |
| | | </Form.Item> |
| | | <Form.Item |
| | | name={['address', 'street']} |
| | | noStyle |
| | | rules={[ |
| | | { |
| | | required: false, |
| | | message: 'Street is required', |
| | | }, |
| | | ]} |
| | | > |
| | | <Input |
| | | style={{ |
| | | width: '50%', |
| | | }} |
| | | placeholder="Input street" |
| | | /> |
| | | </Form.Item> |
| | | </Space.Compact> |
| | | </Form.Item> |
| | | </Col> |
| | | <Col span={24}> |
| | | <Form.Item |
| | | name="phone" |
| | | label="Phone Number" |
| | | rules={[ |
| | | { |
| | | required: false, |
| | | message: 'Please input your phone number!', |
| | | }, |
| | | ]} |
| | | > |
| | | <Input |
| | | addonBefore={prefixSelector} |
| | | style={{ |
| | | width: '100%', |
| | | }} |
| | | /> |
| | | </Form.Item> |
| | | </Col> */} |
| | | |
| | | |
| | | |
| | | </Row> |
| | | </Form> |
| | | </Drawer > |
| | | </> |
| | | ) |
| | | } |
| | | |
| | | export default SpriteSettings; |
| | |
| | | background: transparent; |
| | | } |
| | | |
| | | .ant-float-btn-group { |
| | | position: absolute; |
| | | } |
| | | |
| | | * { |
| | | box-sizing: border-box; |
| | | } |
| | |
| | | import * as React from 'react' |
| | | import * as PIXI from 'pixi.js'; |
| | | import { FormattedMessage, useIntl, useModel } from '@umijs/max'; |
| | | import { Layout, Button, Flex, Row, Col, FloatButton } from 'antd'; |
| | | import { Layout, Button, Flex, Row, Col, FloatButton, Select } from 'antd'; |
| | | const { Header, Content } = Layout; |
| | | import { |
| | | AppstoreAddOutlined, |
| | | FileAddOutlined, |
| | | CompressOutlined, |
| | | SettingOutlined, |
| | | } from '@ant-design/icons'; |
| | | import './index.css' |
| | | import { createStyles } from 'antd-style'; |
| | | import Edit from './components/device' |
| | | import Edit from './components/device'; |
| | | import Settings from './components/settings' |
| | | import * as Utils from './utils' |
| | | import Player from './player'; |
| | | |
| | |
| | | backgroundColor: '#F8FAFB', |
| | | height: 'calc(100vh - 120px)' |
| | | }, |
| | | select: { |
| | | color: 'red', |
| | | fontWeight: 'bold', |
| | | } |
| | | }; |
| | | }); |
| | | |
| | | export const MapModel = Object.freeze({ |
| | | OBSERVER_MODEL: "1", |
| | | MOVABLE_MODEL: "2", |
| | | SETTINGS_MODEL: "3", |
| | | }) |
| | | |
| | | let player; |
| | | |
| | | const Map = () => { |
| | | const intl = useIntl(); |
| | | const { initialState, setInitialState } = useModel('@@initialState'); |
| | | const { styles } = useStyles(); |
| | | const mapRef = React.useRef(); |
| | | const contentRef = React.useRef(); |
| | | |
| | | const [model, setModel] = React.useState(() => MapModel.OBSERVER_MODEL); |
| | | const [deviceVisible, setDeviceVisible] = React.useState(false); |
| | | const [settingsVisible, setSettingsVisible] = React.useState(false); |
| | | const [windowSize, setWindowSize] = React.useState({ |
| | | width: window.innerWidth, |
| | | height: window.innerHeight, |
| | | }); |
| | | const [app, setApp] = React.useState(null); |
| | | const [mapContainer, setMapContainer] = React.useState(null); |
| | | const [mapEditModel, setMapEditModel] = React.useState(false); |
| | | const [didClickSprite, setDidClickSprite] = React.useState(false); |
| | | const [spriteBySettings, setSpriteBySettings] = React.useState(null); |
| | | const prevSpriteBySettingsRef = React.useRef(); |
| | | |
| | | // init func |
| | | React.useEffect(() => { |
| | | player = new Player(mapRef.current, styles.dark); |
| | | player = new Player(mapRef.current, styles.dark, didClickSprite); |
| | | setApp(player.app); |
| | | setMapContainer(player.mapContainer); |
| | | Utils.syncApp(player.app); |
| | |
| | | window.addEventListener('resize', handleResize); |
| | | }, []); |
| | | |
| | | // resize |
| | | React.useEffect(() => { |
| | | if (!app) { |
| | | return; |
| | |
| | | app.renderer.resize(width, height); |
| | | }, [app, mapContainer, windowSize]) |
| | | |
| | | // model |
| | | React.useEffect(() => { |
| | | if (!mapContainer) { |
| | | return; |
| | | } |
| | | if (mapEditModel) { |
| | | player.showGridlines(); |
| | | } else { |
| | | player.hideGridlines(); |
| | | } |
| | | }, [mapEditModel]); |
| | | switch (model) { |
| | | case MapModel.OBSERVER_MODEL: |
| | | |
| | | const onDrop = (sprite, x, y) => { |
| | | player.hideGridlines(); |
| | | player.hideStarryBackground(); |
| | | |
| | | player.activateMapEvent(null); |
| | | |
| | | Utils.removeSelectedEffect(); |
| | | setDeviceVisible(false); |
| | | setSettingsVisible(false); |
| | | |
| | | mapContainer.children.forEach(child => { |
| | | child.off('pointerup'); |
| | | child.off('pointermove'); |
| | | child.off('pointerdown'); |
| | | child.off('click'); |
| | | }) |
| | | break |
| | | case MapModel.MOVABLE_MODEL: |
| | | |
| | | player.showGridlines(); |
| | | player.hideStarryBackground(); |
| | | |
| | | player.activateMapEvent(Utils.MapEvent.SELECTION_BOX); |
| | | |
| | | Utils.removeSelectedEffect(); |
| | | setSpriteBySettings(null); |
| | | setSettingsVisible(false); |
| | | |
| | | mapContainer.children.forEach(child => { |
| | | Utils.beMovable(child, setDidClickSprite); |
| | | }) |
| | | break |
| | | case MapModel.SETTINGS_MODEL: |
| | | |
| | | player.showGridlines(); |
| | | player.showStarryBackground(); |
| | | |
| | | player.activateMapEvent(null); |
| | | |
| | | setDeviceVisible(false); |
| | | |
| | | mapContainer.children.forEach(child => { |
| | | Utils.beSettings(child, setSpriteBySettings, setDidClickSprite); |
| | | }) |
| | | break |
| | | default: |
| | | break |
| | | } |
| | | }, [model]); |
| | | |
| | | // Add New Device |
| | | const onDrop = (sprite, type, x, y) => { |
| | | const { mapX, mapY } = Utils.getRealPosition(x, y, mapContainer); |
| | | sprite.x = mapX; |
| | | sprite.y = mapY; |
| | | |
| | | Utils.initSprite(sprite, type); |
| | | mapContainer.addChild(sprite); |
| | | Utils.beMovable(sprite, setDidClickSprite); |
| | | }; |
| | | |
| | | // didClickSprite, stop triggers both sprite click and play's selection boxs |
| | | React.useEffect(() => { |
| | | player.updateDidClickSprite(didClickSprite); |
| | | }, [didClickSprite]) |
| | | |
| | | // watch spriteBySettings |
| | | React.useEffect(() => { |
| | | if (!mapContainer) { |
| | | return; |
| | | } |
| | | prevSpriteBySettingsRef.current = spriteBySettings; |
| | | if (spriteBySettings && prevSpriteBySettings !== spriteBySettings) { |
| | | Utils.removeSelectedEffect(); |
| | | } |
| | | if (spriteBySettings) { |
| | | Utils.showSelectedEffect(spriteBySettings) |
| | | setSettingsVisible(true); |
| | | } else { |
| | | Utils.removeSelectedEffect(); |
| | | } |
| | | }, [spriteBySettings]) |
| | | const prevSpriteBySettings = prevSpriteBySettingsRef.current; |
| | | |
| | | const settingsFinish = () => { |
| | | setSettingsVisible(false); |
| | | setSpriteBySettings(null); |
| | | } |
| | | |
| | | return ( |
| | | <> |
| | |
| | | <Col span={8} style={{ backgroundColor: '#3C40C6' }}></Col> |
| | | <Col span={16} style={{ backgroundColor: '#3C40C6' }}> |
| | | <Flex className={styles.flex} gap={'large'} justify={'flex-end'} align={'center'}> |
| | | <Button onClick={() => setMapEditModel(!mapEditModel)} size={'large'}> |
| | | {!mapEditModel |
| | | ? <span style={{ fontWeight: 'bold' }}><FormattedMessage id='map.edit' defaultMessage='编辑地图' /></span> |
| | | : <span style={{ color: 'red', fontWeight: 'bold' }}><FormattedMessage id='map.edit.close' defaultMessage='退出编辑' /></span> |
| | | } |
| | | </Button> |
| | | <Select |
| | | className={styles.select} |
| | | size={'large'} |
| | | defaultValue={MapModel.OBSERVER_MODEL} |
| | | style={{ |
| | | width: 180, |
| | | }} |
| | | onChange={setModel} |
| | | options={[ |
| | | { |
| | | value: MapModel.OBSERVER_MODEL, |
| | | label: intl.formatMessage({ id: 'map.model.observer', defaultMessage: '观察者模式' }), |
| | | }, |
| | | { |
| | | value: MapModel.MOVABLE_MODEL, |
| | | label: intl.formatMessage({ id: 'map.model.editor', defaultMessage: '编辑者模式' }), |
| | | }, |
| | | ]} |
| | | /> |
| | | </Flex> |
| | | </Col> |
| | | </Row> |
| | | </Header> |
| | | <Content ref={contentRef} className={styles.content}> |
| | | <div ref={mapRef} style={{ position: "relative" }} /> |
| | | |
| | | <div ref={mapRef} style={{ position: "relative" }} > |
| | | <FloatButton.Group |
| | | shape="square" |
| | | style={{ |
| | |
| | | </FloatButton.Group> |
| | | |
| | | <FloatButton.Group |
| | | hidden={!mapEditModel} |
| | | trigger="hover" |
| | | hidden={model === MapModel.OBSERVER_MODEL} |
| | | style={{ |
| | | right: 35, |
| | | bottom: 35 |
| | | left: 35, |
| | | bottom: window.innerHeight / 2 |
| | | }} |
| | | icon={<AppstoreAddOutlined />} |
| | | > |
| | | <FloatButton |
| | | hidden={model === MapModel.OBSERVER_MODEL} |
| | | type={deviceVisible ? 'primary' : 'default'} |
| | | tooltip={<div><FormattedMessage id='map.device.add' defaultMessage='添加设备' /></div>} |
| | | icon={<FileAddOutlined />} |
| | | onClick={() => { |
| | | if (deviceVisible) { |
| | | setDeviceVisible(false); |
| | | } else { |
| | | setDeviceVisible(true); |
| | | setModel(MapModel.MOVABLE_MODEL); |
| | | } |
| | | }} |
| | | /> |
| | | <FloatButton |
| | | hidden={model === MapModel.OBSERVER_MODEL} |
| | | type={model === MapModel.SETTINGS_MODEL ? 'primary' : 'default'} |
| | | tooltip={<div><FormattedMessage id='map.device.oper' defaultMessage='参数设置' /></div>} |
| | | icon={<SettingOutlined />} |
| | | onClick={() => { |
| | | setModel(model === MapModel.SETTINGS_MODEL ? MapModel.MOVABLE_MODEL : MapModel.SETTINGS_MODEL) |
| | | }} |
| | | /> |
| | | </FloatButton.Group> |
| | | </div> |
| | | </Content> |
| | | </Layout> |
| | | |
| | |
| | | refCurr={mapRef.current} |
| | | onDrop={onDrop} |
| | | /> |
| | | |
| | | <Settings |
| | | open={settingsVisible} |
| | | curSprite={spriteBySettings} |
| | | onCancel={() => { |
| | | setSettingsVisible(false); |
| | | setSpriteBySettings(null); |
| | | }} |
| | | refCurr={mapRef.current} |
| | | onSubmit={settingsFinish} |
| | | /> |
| | | </> |
| | | ) |
| | | } |
| | |
| | | import * as PIXI from 'pixi.js'; |
| | | import * as TWEEDLE from 'tweedle.js'; |
| | | import * as Utils from './utils' |
| | | import star from '/public/img/map/star.png' |
| | | |
| | | export default class Player { |
| | | |
| | | constructor(dom, dark) { |
| | | constructor(dom, dark, didClickSprite) { |
| | | // not dynamic |
| | | this.darkModel = dark; |
| | | this.didClickSprite = didClickSprite; |
| | | // init |
| | | this.app = generatePixiApp(dark); |
| | | dom.appendChild(this.app.view); |
| | |
| | | |
| | | this.mapContainer = generatePixiContainer('mapContainer'); |
| | | this.app.stage.addChild(this.mapContainer); |
| | | this.app.view.addEventListener('contextmenu', (event) => { |
| | | event.preventDefault(); |
| | | }); |
| | | |
| | | this.scale = 1; // 缩放 |
| | | this.pan = false; // 平移 |
| | | |
| | | // func |
| | | this.app.view.addEventListener('mousedown', (event) => { |
| | | // 右键 |
| | | if (event.button === 2) { |
| | | this.mapPan(event); |
| | | } |
| | | }) |
| | | // this.activateMapEvent(null); |
| | | this.activateMapScale(); |
| | | this.activateMapPan(); |
| | | this.showCoordinates(); |
| | | this.appTicker(); |
| | | } |
| | | |
| | | activateMapScale = () => { |
| | | this.app.view.addEventListener('wheel', (event) => { |
| | | event.preventDefault(); |
| | | const delta = Math.sign(event.deltaY); |
| | | |
| | | if (delta === 1) { |
| | | this.scale *= 0.9; |
| | | } else if (delta === -1) { |
| | | this.scale *= 1.1; |
| | | } |
| | | this.mapContainer.scale.set(this.scale); |
| | | this.mapContainer.children.forEach(child => { |
| | | // child.scale.set(1 / this.scale); // 防止图标变小 |
| | | activateMapEvent = (leftEvent, rightEvent) => { |
| | | if (this.mapEvent) { |
| | | this.mapContainer.parent.off('mousedown'); |
| | | this.mapEvent = null; |
| | | if (this.selectedSprites && this.selectedSprites.length > 0) { |
| | | this.selectedSprites.forEach(child => { |
| | | Utils.unMarkSprite(child); |
| | | }) |
| | | } |
| | | } |
| | | this.mapEvent = (event) => { |
| | | if (leftEvent && event.button === 0) { |
| | | switch (leftEvent) { |
| | | case Utils.MapEvent.SELECTION_BOX: |
| | | this.mapSelect(event); |
| | | break |
| | | default: |
| | | break |
| | | } |
| | | } |
| | | if (rightEvent && event.button === 2) { |
| | | switch (rightEvent) { |
| | | default: |
| | | break |
| | | } |
| | | } |
| | | } |
| | | this.mapContainer.parent.on('mousedown', this.mapEvent) |
| | | } |
| | | |
| | | mapSelect = (event) => { |
| | | let isSelecting = false; |
| | | if (!this.selectionBox) { |
| | | this.selectionBox = new PIXI.Graphics(); |
| | | this.app.stage.addChild(this.selectionBox); |
| | | } |
| | | |
| | | // select start pos |
| | | const startPoint = new PIXI.Point(); |
| | | this.app.renderer.events.mapPositionToPoint(startPoint, event.clientX, event.clientY); |
| | | let selectionStart = { x: startPoint.x, y: startPoint.y }; |
| | | |
| | | isSelecting = true; |
| | | |
| | | const handleMouseMove = (event) => { |
| | | if (isSelecting && !this.didClickSprite) { |
| | | // select end pos |
| | | const endPoint = new PIXI.Point(); |
| | | this.app.renderer.events.mapPositionToPoint(endPoint, event.clientX, event.clientY); |
| | | const selectionEnd = { x: endPoint.x, y: endPoint.y } |
| | | |
| | | const width = Math.abs(selectionEnd.x - selectionStart.x); |
| | | const height = Math.abs(selectionEnd.y - selectionStart.y); |
| | | |
| | | this.selectionBox.clear(); |
| | | this.selectionBox.lineStyle(2, 0xCCCCCC, 1); |
| | | this.selectionBox.beginFill(0xCCCCCC, 0.2); |
| | | this.selectionBox.drawRect(Math.min(selectionStart.x, selectionEnd.x), Math.min(selectionStart.y, selectionEnd.y), width, height); |
| | | this.selectionBox.endFill(); |
| | | } |
| | | } |
| | | |
| | | this.mapContainer.parent.on('mousemove', handleMouseMove); |
| | | |
| | | this.mapContainer.parent.on('mouseup', (event) => { |
| | | if (isSelecting) { |
| | | // sprite show style which be selected |
| | | if (this.selectedSprites && this.selectedSprites.length > 0) { |
| | | this.selectedSprites.forEach(child => { |
| | | Utils.unMarkSprite(child); |
| | | }) |
| | | } |
| | | this.selectedSprites = []; |
| | | |
| | | this.mapContainer.children.forEach(child => { |
| | | if (Utils.isSpriteInSelectionBox(child, this.selectionBox)) { |
| | | this.selectedSprites.push(child); |
| | | Utils.markSprite(child); |
| | | } |
| | | }) |
| | | isSelecting = false; |
| | | this.selectionBox.clear(); |
| | | |
| | | // sprites batch move |
| | | Utils.spriteListBeMovable(this.selectedSprites, this.scale, () => { |
| | | this.activateMapEvent(Utils.MapEvent.SELECTION_BOX); |
| | | }); |
| | | |
| | | } |
| | | |
| | | this.mapContainer.parent.off('mousemove', handleMouseMove); |
| | | }); |
| | | } |
| | | |
| | | mapPan = (event) => { |
| | | activateMapPan = () => { |
| | | const mapPanHandle = (event) => { |
| | | if (event.button === 2) { |
| | | this.pan = true; |
| | | let previousPosition = { x: event.clientX, y: event.clientY }; |
| | | const mouseMoveHandler = (event) => { |
| | |
| | | this.app.view.addEventListener('mouseup', () => { |
| | | this.app.view.removeEventListener('mousemove', mouseMoveHandler); |
| | | this.pan = false; |
| | | }); |
| | | } |
| | | } |
| | | this.app.view.addEventListener('mousedown', mapPanHandle); |
| | | } |
| | | |
| | | activateMapScale = () => { |
| | | this.scale = 1; // 缩放 |
| | | this.app.view.addEventListener('wheel', (event) => { |
| | | event.preventDefault(); |
| | | const delta = Math.sign(event.deltaY); |
| | | |
| | | if (delta === 1) { |
| | | this.scale *= 0.9; |
| | | } else if (delta === -1) { |
| | | this.scale *= 1.1; |
| | | } |
| | | this.mapContainer.scale.set(this.scale); |
| | | this.mapContainer.children.forEach(child => { |
| | | // child.scale.set(1 / this.scale); // 防止图标变小 |
| | | }) |
| | | }); |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | showStarryBackground = () => { |
| | | if (!this.starryContainer) { |
| | | this.starryContainer = generatePixiContainer('starryContainer'); |
| | | this.app.stage.addChild(this.starryContainer); |
| | | } |
| | | |
| | | const starTexture = PIXI.Texture.from(star); |
| | | |
| | | const starAmount = 300; |
| | | let cameraZ = 0; |
| | | const fov = 20; |
| | | const baseSpeed = 0.025; |
| | | let speed = 0; |
| | | let warpSpeed = 1; |
| | | const starStretch = 5; |
| | | const starBaseSize = 0.05; |
| | | |
| | | const stars = []; |
| | | |
| | | for (let i = 0; i < starAmount; i++) { |
| | | const star = { |
| | | sprite: new PIXI.Sprite(starTexture), |
| | | z: 0, |
| | | x: 0, |
| | | y: 0, |
| | | }; |
| | | star.sprite.anchor.x = 0.5; |
| | | star.sprite.anchor.y = 0.7; |
| | | star.sprite.tint = 0x8395a7; // filter |
| | | randomizeStar(star, true); |
| | | this.starryContainer.addChild(star.sprite); |
| | | stars.push(star); |
| | | } |
| | | |
| | | this.starryInterval = setInterval(() => { |
| | | warpSpeed = warpSpeed > 0 ? 0 : 1; |
| | | }, 5000); |
| | | |
| | | |
| | | this.starryTicker = (delta) => { |
| | | speed += (warpSpeed - speed) / 20; |
| | | cameraZ += delta * 10 * (speed + baseSpeed); |
| | | for (let i = 0; i < starAmount; i++) { |
| | | const star = stars[i]; |
| | | if (star.z < cameraZ) randomizeStar(star); |
| | | |
| | | const z = star.z - cameraZ; |
| | | star.sprite.x = star.x * (fov / z) * this.app.renderer.screen.width + this.app.renderer.screen.width / 2; |
| | | star.sprite.y = star.y * (fov / z) * this.app.renderer.screen.width + this.app.renderer.screen.height / 2; |
| | | |
| | | const dxCenter = star.sprite.x - this.app.renderer.screen.width / 2; |
| | | const dyCenter = star.sprite.y - this.app.renderer.screen.height / 2; |
| | | const distanceCenter = Math.sqrt(dxCenter * dxCenter + dyCenter * dyCenter); |
| | | const distanceScale = Math.max(0, (2000 - z) / 2000); |
| | | star.sprite.scale.x = distanceScale * starBaseSize; |
| | | |
| | | star.sprite.scale.y = distanceScale * starBaseSize + distanceScale * speed * starStretch * distanceCenter / this.app.renderer.screen.width; |
| | | star.sprite.rotation = Math.atan2(dyCenter, dxCenter) + Math.PI / 2; |
| | | } |
| | | } |
| | | |
| | | this.app.ticker.add(this.starryTicker); |
| | | |
| | | function randomizeStar(star, initial) { |
| | | star.z = initial ? Math.random() * 2000 : cameraZ + Math.random() * 1000 + 2000; |
| | | |
| | | const deg = Math.random() * Math.PI * 2; |
| | | const distance = Math.random() * 50 + 1; |
| | | star.x = Math.cos(deg) * distance; |
| | | star.y = Math.sin(deg) * distance; |
| | | } |
| | | } |
| | | |
| | | hideStarryBackground = () => { |
| | | if(this.starryTicker) { |
| | | this.app.ticker.remove(this.starryTicker); |
| | | this.starryTicker = null; |
| | | } |
| | | |
| | | if (this.starryInterval) { |
| | | clearInterval(this.starryInterval); |
| | | this.starryInterval = null; |
| | | } |
| | | |
| | | if (this.starryContainer) { |
| | | this.starryContainer.removeChildren(); |
| | | this.app.stage.removeChild(this.starryContainer); |
| | | this.starryContainer = null; |
| | | } |
| | | } |
| | | |
| | | updateDidClickSprite = (value) => { |
| | | this.didClickSprite = value; |
| | | } |
| | | |
| | | appTicker = () => { |
| | | TWEEDLE.Group.shared.update(); |
| | | } |
| | |
| | | background: dark ? '#f1f2f6' : '#f1f2f6', |
| | | antialias: true, |
| | | }) |
| | | app.stage.eventMode = 'auto'; |
| | | app.stage.eventMode = 'static'; |
| | | app.stage.hitArea = app.screen; |
| | | app.view.addEventListener('contextmenu', (event) => { |
| | | event.preventDefault(); |
| | | }); |
| | | return app; |
| | | } |
| | | |
| | |
| | | |
| | | |
| | | import * as PIXI from 'pixi.js'; |
| | | |
| | | let app = null; |
| | | let mapContainer = null; |
| | | let effectTick, effectHalfCircle, effectRectangle; |
| | | |
| | | export function syncApp(param) { |
| | | app = param; |
| | |
| | | mapContainer = param; |
| | | } |
| | | |
| | | export const MapEvent = Object.freeze({ |
| | | SELECTION_BOX: Symbol.for(0), |
| | | }) |
| | | |
| | | export const getRealPosition = (x, y, mapContainer) => { |
| | | const rect = app.view.getBoundingClientRect(); |
| | | return { |
| | |
| | | mapY: (y - rect.top) / mapContainer.scale.y - mapContainer.y / mapContainer.scale.y |
| | | } |
| | | } |
| | | |
| | | export const initSprite = (sprite, type) => { |
| | | sprite.anchor.set(0.5); |
| | | sprite.cursor = 'pointer'; |
| | | sprite.eventMode = 'static'; |
| | | sprite.data = { |
| | | type: type |
| | | }; |
| | | } |
| | | |
| | | // sprite be movable from sprite click event |
| | | export const beMovable = (sprite, setDidClickSprite) => { |
| | | sprite.off('pointerup'); |
| | | sprite.off('pointermove'); |
| | | sprite.off('pointerdown'); |
| | | sprite.off('click'); |
| | | |
| | | sprite.on("pointerdown", onDragStart); |
| | | |
| | | let dragTarget; |
| | | function onDragStart(event) { |
| | | setDidClickSprite(true); |
| | | dragTarget = event.currentTarget; |
| | | mapContainer.parent.off('pointermove'); |
| | | mapContainer.parent.on('pointermove', onDragMove, dragTarget); |
| | | |
| | | mapContainer.parent.off('pointerup'); |
| | | mapContainer.parent.on('pointerup', onDragEnd.bind(mapContainer)); |
| | | } |
| | | |
| | | function onDragMove(event) { |
| | | if (this) { |
| | | this.parent.toLocal(event.global, null, this.position); |
| | | } |
| | | } |
| | | |
| | | function onDragEnd() { |
| | | if (dragTarget) { |
| | | setDidClickSprite(false); |
| | | this.parent.off('pointermove'); |
| | | this.parent.off('pointerup'); |
| | | dragTarget.alpha = 1; |
| | | dragTarget = null; |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | // sprite be beSettings from sprite click event |
| | | export const beSettings = (sprite, setSpriteBySettings, setDidClickSprite) => { |
| | | sprite.off('pointerup'); |
| | | sprite.off('pointermove'); |
| | | sprite.off('pointerdown'); |
| | | sprite.off('click'); |
| | | |
| | | sprite.on("click", onClick); |
| | | |
| | | function onClick(event) { |
| | | setSpriteBySettings(sprite); |
| | | // setDidClickSprite(true); |
| | | } |
| | | } |
| | | |
| | | // sprites be movable from select box |
| | | // the scale was dynamic |
| | | export const spriteListBeMovable = (selectedSprites, scale, resetFn) => { |
| | | if (selectedSprites && selectedSprites.length > 0) { |
| | | let batchMove = false; |
| | | let batchMoveStartPos = null; |
| | | |
| | | const batchMoving = (event) => { |
| | | if (batchMove && batchMoveStartPos) { |
| | | // offset move val |
| | | var mouseMovement = { |
| | | x: (event.global.x - batchMoveStartPos.x) / scale, |
| | | y: (event.global.y - batchMoveStartPos.y) / scale |
| | | }; |
| | | for (let sprite of selectedSprites) { |
| | | sprite.position.x = sprite.data.batchMoveStartPos.x + mouseMovement.x; |
| | | sprite.position.y = sprite.data.batchMoveStartPos.y + mouseMovement.y; |
| | | } |
| | | } |
| | | } |
| | | |
| | | const batchMoveEnd = (event) => { |
| | | batchMove = false; |
| | | batchMoveStartPos = null; |
| | | selectedSprites.forEach(child => { |
| | | unMarkSprite(child); |
| | | }) |
| | | selectedSprites = []; |
| | | mapContainer.parent.off('mousedown'); |
| | | mapContainer.parent.off('mousemove'); |
| | | mapContainer.parent.off('mouseup'); |
| | | |
| | | resetFn(); |
| | | } |
| | | |
| | | const batchMoveStart = (event) => { |
| | | batchMoveStartPos = { x: event.data.global.clone().x, y: event.data.global.clone().y }; |
| | | selectedSprites.forEach(child => { |
| | | child.data.batchMoveStartPos = { x: child.position.x, y: child.position.y }; |
| | | }) |
| | | |
| | | batchMove = true; |
| | | mapContainer.parent.off('mousemove'); |
| | | mapContainer.parent.on('mousemove', batchMoving); |
| | | |
| | | mapContainer.parent.off('mouseup'); |
| | | mapContainer.parent.on('mouseup', batchMoveEnd); |
| | | } |
| | | |
| | | mapContainer.parent.off('mousedown') |
| | | mapContainer.parent.on('mousedown', batchMoveStart); |
| | | } |
| | | } |
| | | |
| | | export const isSpriteInSelectionBox = (sprite, selectionBox) => { |
| | | const spriteBounds = sprite.getBounds(); |
| | | const boxBounds = selectionBox.getBounds(); |
| | | |
| | | return spriteBounds.x + spriteBounds.width > boxBounds.x |
| | | && spriteBounds.x < boxBounds.x + boxBounds.width |
| | | && spriteBounds.y + spriteBounds.height > boxBounds.y |
| | | && spriteBounds.y < boxBounds.y + boxBounds.height; |
| | | } |
| | | |
| | | export const showSelectedEffect = (sprite) => { |
| | | const { width, height } = sprite; |
| | | const scale = sprite.scale.x; |
| | | const sideLen = (Math.max(width, height) + 10) * scale; |
| | | const color = 0x273c75; |
| | | |
| | | effectHalfCircle = new PIXI.Graphics(); |
| | | effectHalfCircle.beginFill(color); |
| | | effectHalfCircle.lineStyle(2 * scale, color); |
| | | effectHalfCircle.arc(0, 0, sideLen, 0, Math.PI); |
| | | effectHalfCircle.endFill(); |
| | | effectHalfCircle.position.set(sprite.x, sprite.y); |
| | | effectHalfCircle.scale.set(1 / scale); |
| | | |
| | | effectRectangle = new PIXI.Graphics(); |
| | | effectRectangle.lineStyle(2 * scale, color, 1); |
| | | effectRectangle.drawRoundedRect(0, 0, sideLen, sideLen, 16 * scale); |
| | | effectRectangle.endFill(); |
| | | effectRectangle.mask = effectHalfCircle; |
| | | |
| | | const scaledWidth = sideLen * (1 / scale); |
| | | const scaledHeight = sideLen * (1 / scale); |
| | | |
| | | effectRectangle.scale.set(1 / scale); |
| | | effectRectangle.position.set(sprite.x - scaledWidth / 2, sprite.y - scaledHeight / 2); |
| | | |
| | | mapContainer.addChild(effectRectangle); |
| | | mapContainer.addChild(effectHalfCircle); |
| | | |
| | | let phase = 0; |
| | | effectTick = (delta) => { |
| | | phase += delta / 10; |
| | | phase %= (Math.PI * 2); |
| | | effectHalfCircle.rotation = phase; |
| | | }; |
| | | |
| | | app.ticker.add(effectTick); |
| | | } |
| | | |
| | | export const removeSelectedEffect = () => { |
| | | if (effectTick) { |
| | | app.ticker.remove(effectTick); |
| | | } |
| | | if (effectHalfCircle) { |
| | | mapContainer.removeChild(effectHalfCircle); |
| | | effectHalfCircle = null; |
| | | } |
| | | if (effectRectangle) { |
| | | mapContainer.removeChild(effectRectangle); |
| | | effectRectangle = null; |
| | | } |
| | | } |
| | | |
| | | export const markSprite = (sprite) => { |
| | | sprite.alpha = 0.5; |
| | | } |
| | | |
| | | export const unMarkSprite = (sprite) => { |
| | | sprite.alpha = 1; |
| | | } |