From 17150b54d35c3ab02b2082ac4e9fc34858d43d77 Mon Sep 17 00:00:00 2001
From: Junjie <xjj@123>
Date: 星期一, 11 三月 2024 08:38:41 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'

---
 zy-asrs-flow/src/locales/en-US.ts                  |    1 
 zy-asrs-flow/src/pages/map/player.js               |  263 +++++++++++-
 zy-asrs-flow/src/pages/map/index.css               |    4 
 zy-asrs-flow/src/locales/en-US/map.ts              |   17 
 zy-asrs-flow/public/img/map/star.png               |    0 
 zy-asrs-flow/src/pages/map/components/settings.jsx |  430 +++++++++++++++++++++
 zy-asrs-flow/src/pages/map/index.jsx               |  232 +++++++++--
 zy-asrs-flow/src/pages/map/components/device.jsx   |   38 +
 zy-asrs-flow/src/pages/map/utils.js                |  195 +++++++++
 9 files changed, 1,076 insertions(+), 104 deletions(-)

diff --git a/zy-asrs-flow/public/img/map/star.png b/zy-asrs-flow/public/img/map/star.png
new file mode 100644
index 0000000..a2b74e8
--- /dev/null
+++ b/zy-asrs-flow/public/img/map/star.png
Binary files differ
diff --git a/zy-asrs-flow/src/locales/en-US.ts b/zy-asrs-flow/src/locales/en-US.ts
index 654c3e5..7e1627b 100644
--- a/zy-asrs-flow/src/locales/en-US.ts
+++ b/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',
   '':'',
   '':'',
   '':'',
diff --git a/zy-asrs-flow/src/locales/en-US/map.ts b/zy-asrs-flow/src/locales/en-US/map.ts
index 0926568..dfb20d9 100644
--- a/zy-asrs-flow/src/locales/en-US/map.ts
+++ b/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',
+    '': '',
+    '': '',
+    '': '',
+    '': '',
 }
\ No newline at end of file
diff --git a/zy-asrs-flow/src/pages/map/components/device.jsx b/zy-asrs-flow/src/pages/map/components/device.jsx
index 81c69af..89fc6b9 100644
--- a/zy-asrs-flow/src/pages/map/components/device.jsx
+++ b/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>
diff --git a/zy-asrs-flow/src/pages/map/components/settings.jsx b/zy-asrs-flow/src/pages/map/components/settings.jsx
new file mode 100644
index 0000000..3763c34
--- /dev/null
+++ b/zy-asrs-flow/src/pages/map/components/settings.jsx
@@ -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;
\ No newline at end of file
diff --git a/zy-asrs-flow/src/pages/map/index.css b/zy-asrs-flow/src/pages/map/index.css
index bdd99ce..6c27554 100644
--- a/zy-asrs-flow/src/pages/map/index.css
+++ b/zy-asrs-flow/src/pages/map/index.css
@@ -11,6 +11,10 @@
     background: transparent;
 }
 
+.ant-float-btn-group {
+    position: absolute;
+}
+
 * {
     box-sizing: border-box;
 }
diff --git a/zy-asrs-flow/src/pages/map/index.jsx b/zy-asrs-flow/src/pages/map/index.jsx
index 7956a63..00e3809 100644
--- a/zy-asrs-flow/src/pages/map/index.jsx
+++ b/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,49 +204,77 @@
                         <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" }} />
-
-                    <FloatButton.Group
-                        shape="square"
-                        style={{
-                            left: 35,
-                            bottom: 35
-                        }}
-                    >
-                        <FloatButton
-                            icon={<CompressOutlined />}
-                        />
-                        <FloatButton.BackTop visibilityHeight={0} />
-                    </FloatButton.Group>
-
-                    <FloatButton.Group
-                        hidden={!mapEditModel}
-                        trigger="hover"
-                        style={{
-                            right: 35,
-                            bottom: 35
-                        }}
-                        icon={<AppstoreAddOutlined />}
-                    >
-                        <FloatButton
-                            tooltip={<div><FormattedMessage id='map.device.add' defaultMessage='娣诲姞璁惧' /></div>}
-                            icon={<FileAddOutlined />}
-                            onClick={() => {
-                                setDeviceVisible(true);
+                    <div ref={mapRef} style={{ position: "relative" }} >
+                        <FloatButton.Group
+                            shape="square"
+                            style={{
+                                left: 35,
+                                bottom: 35
                             }}
-                        />
-                    </FloatButton.Group>
+                        >
+                            <FloatButton
+                                icon={<CompressOutlined />}
+                            />
+                            <FloatButton.BackTop visibilityHeight={0} />
+                        </FloatButton.Group>
+
+                        <FloatButton.Group
+                            hidden={model === MapModel.OBSERVER_MODEL}
+                            style={{
+                                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}
+            />
         </>
     )
 }
diff --git a/zy-asrs-flow/src/pages/map/player.js b/zy-asrs-flow/src/pages/map/player.js
index 4f2e7b3..35edc66 100644
--- a/zy-asrs-flow/src/pages/map/player.js
+++ b/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,26 +17,136 @@
 
         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();
     }
 
+    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);
+        });
+    }
+
+    activateMapPan = () => {
+        const mapPanHandle = (event) => {
+            if (event.button === 2) {
+                this.pan = true;
+                let previousPosition = { x: event.clientX, y: event.clientY };
+                const mouseMoveHandler = (event) => {
+                    if (this.pan) {
+                        const dx = event.clientX - previousPosition.x;
+                        const dy = event.clientY - previousPosition.y;
+
+                        this.mapContainer.position.x += dx;
+                        this.mapContainer.position.y += dy;
+
+                        previousPosition = { x: event.clientX, y: event.clientY };
+                    }
+                };
+                this.app.view.addEventListener('mousemove', mouseMoveHandler);
+                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);
@@ -46,27 +160,6 @@
             this.mapContainer.children.forEach(child => {
                 // child.scale.set(1 / this.scale); // 闃叉鍥炬爣鍙樺皬
             })
-        });
-    }
-
-    mapPan = (event) => {
-        this.pan = true;
-        let previousPosition = { x: event.clientX, y: event.clientY };
-        const mouseMoveHandler = (event) => {
-            if (this.pan) {
-                const dx = event.clientX - previousPosition.x;
-                const dy = event.clientY - previousPosition.y;
-
-                this.mapContainer.position.x += dx;
-                this.mapContainer.position.y += dy;
-
-                previousPosition = { x: event.clientX, y: event.clientY };
-            }
-        };
-        this.app.view.addEventListener('mousemove', mouseMoveHandler);
-        this.app.view.addEventListener('mouseup', () => {
-            this.app.view.removeEventListener('mousemove', mouseMoveHandler);
-            this.pan = false;
         });
     }
 
@@ -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;
 }
 
diff --git a/zy-asrs-flow/src/pages/map/utils.js b/zy-asrs-flow/src/pages/map/utils.js
index 3abb061..c01b8d8 100644
--- a/zy-asrs-flow/src/pages/map/utils.js
+++ b/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,10 +12,201 @@
     mapContainer = param;
 }
 
+export const MapEvent = Object.freeze({
+    SELECTION_BOX: Symbol.for(0),
+})
+
 export const getRealPosition = (x, y, mapContainer) => {
     const rect = app.view.getBoundingClientRect();
     return {
         mapX: (x - rect.left) / mapContainer.scale.x - mapContainer.x / mapContainer.scale.x,
         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;
 }
\ No newline at end of file

--
Gitblit v1.9.1