Junjie
2024-03-11 17150b54d35c3ab02b2082ac4e9fc34858d43d77
Merge remote-tracking branch 'origin/master'
7个文件已修改
2个文件已添加
1116 ■■■■■ 已修改文件
zy-asrs-flow/public/img/map/star.png 补丁 | 查看 | 原始文档 | blame | 历史
zy-asrs-flow/src/locales/en-US.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-asrs-flow/src/locales/en-US/map.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-asrs-flow/src/pages/map/components/device.jsx 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-asrs-flow/src/pages/map/components/settings.jsx 430 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-asrs-flow/src/pages/map/index.css 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-asrs-flow/src/pages/map/index.jsx 182 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-asrs-flow/src/pages/map/player.js 249 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-asrs-flow/src/pages/map/utils.js 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-asrs-flow/public/img/map/star.png
zy-asrs-flow/src/locales/en-US.ts
@@ -18,6 +18,7 @@
  'common.realname':'Real Name',
  'common.idcard':'ID Number',
  'common.introduction':'Introduction',
  'common.execute':'Execute',
  '':'',
  '':'',
  '':'',
zy-asrs-flow/src/locales/en-US/map.ts
@@ -2,10 +2,27 @@
    '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',
    '': '',
    '': '',
    '': '',
    '': '',
}
zy-asrs-flow/src/pages/map/components/device.jsx
@@ -33,30 +33,32 @@
});
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 (
        <>
@@ -83,7 +85,7 @@
                                width='50px'
                                preview={false}
                                draggable="true"
                                onDragStart={onDragStart}
                                onDragStart={(e) => onDragStart(e, 'AGV')}
                            />
                            <div>AGV</div>
                        </Col>
@@ -93,7 +95,7 @@
                                width='50px'
                                preview={false}
                                draggable="true"
                                onDragStart={onDragStart}
                                onDragStart={(e) => onDragStart(e, 'AGV')}
                            />
                            <div>AGV</div>
                        </Col>
@@ -103,7 +105,7 @@
                                width='50px'
                                preview={false}
                                draggable="true"
                                onDragStart={onDragStart}
                                onDragStart={(e) => onDragStart(e, 'AGV')}
                            />
                            <div>AGV</div>
                        </Col>
@@ -115,7 +117,7 @@
                                width='50px'
                                preview={false}
                                draggable="true"
                                onDragStart={onDragStart}
                                onDragStart={(e) => onDragStart(e, 'AGV')}
                            />
                            <div>AGV</div>
                        </Col>
@@ -125,7 +127,7 @@
                                width='50px'
                                preview={false}
                                draggable="true"
                                onDragStart={onDragStart}
                                onDragStart={(e) => onDragStart(e, 'AGV')}
                            />
                            <div>AGV</div>
                        </Col>
@@ -135,7 +137,7 @@
                                width='50px'
                                preview={false}
                                draggable="true"
                                onDragStart={onDragStart}
                                onDragStart={(e) => onDragStart(e, 'AGV')}
                            />
                            <div>AGV</div>
                        </Col>
@@ -147,7 +149,7 @@
                                width='50px'
                                preview={false}
                                draggable="true"
                                onDragStart={onDragStart}
                                onDragStart={(e) => onDragStart(e, 'AGV')}
                            />
                            <div>AGV</div>
                        </Col>
@@ -157,7 +159,7 @@
                                width='50px'
                                preview={false}
                                draggable="true"
                                onDragStart={onDragStart}
                                onDragStart={(e) => onDragStart(e, 'AGV')}
                            />
                            <div>AGV</div>
                        </Col>
@@ -167,7 +169,7 @@
                                width='50px'
                                preview={false}
                                draggable="true"
                                onDragStart={onDragStart}
                                onDragStart={(e) => onDragStart(e, 'AGV')}
                            />
                            <div>AGV</div>
                        </Col>
zy-asrs-flow/src/pages/map/components/settings.jsx
New file
@@ -0,0 +1,430 @@
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;
zy-asrs-flow/src/pages/map/index.css
@@ -11,6 +11,10 @@
    background: transparent;
}
.ant-float-btn-group {
    position: absolute;
}
* {
    box-sizing: border-box;
}
zy-asrs-flow/src/pages/map/index.jsx
@@ -1,16 +1,18 @@
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';
@@ -36,28 +38,44 @@
            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);
@@ -72,6 +90,7 @@
        window.addEventListener('resize', handleResize);
    }, []);
    // resize
    React.useEffect(() => {
        if (!app) {
            return;
@@ -81,23 +100,101 @@
        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 (
        <>
@@ -107,19 +204,31 @@
                        <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={{
@@ -134,22 +243,38 @@
                    </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>
@@ -161,6 +286,17 @@
                refCurr={mapRef.current}
                onDrop={onDrop}
            />
            <Settings
                open={settingsVisible}
                curSprite={spriteBySettings}
                onCancel={() => {
                    setSettingsVisible(false);
                    setSpriteBySettings(null);
                }}
                refCurr={mapRef.current}
                onSubmit={settingsFinish}
            />
        </>
    )
}
zy-asrs-flow/src/pages/map/player.js
@@ -1,10 +1,14 @@
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);
@@ -13,43 +17,111 @@
        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) => {
@@ -67,6 +139,27 @@
        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); // 防止图标变小
            })
        });
    }
@@ -127,6 +220,101 @@
        }
    }
    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();
    }
@@ -138,8 +326,11 @@
        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;
}
zy-asrs-flow/src/pages/map/utils.js
@@ -1,8 +1,8 @@
import * as PIXI from 'pixi.js';
let app = null;
let mapContainer = null;
let effectTick, effectHalfCircle, effectRectangle;
export function syncApp(param) {
    app = param;
@@ -12,6 +12,10 @@
    mapContainer = param;
}
export const MapEvent = Object.freeze({
    SELECTION_BOX: Symbol.for(0),
})
export const getRealPosition = (x, y, mapContainer) => {
    const rect = app.view.getBoundingClientRect();
    return {
@@ -19,3 +23,190 @@
        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;
}