# Marker2D标记
[Marker2D](https://vjmap.com/map3d/api/classes/Marker2D.html)是基于threejs中 `CSS2DObject` 实现的div标记。

## 常用方法

|方法|描述|
|:---|:---|
|[isShow](https://vjmap.com/map3d/api/classes/Marker2D.html#isShow)|是否显示|
|[show](https://vjmap.com/map3d/api/classes/Marker2D.html#show)|显示|
|[hide](https://vjmap.com/map3d/api/classes/Marker2D.html#hide)|隐藏|
|[getPosition](https://vjmap.com/map3d/api/classes/Marker2D.html#getPosition)|获取位置|
|[setPosition](https://vjmap.com/map3d/api/classes/Marker2D.html#setPosition)|设置位置|
|[togglePopup](https://vjmap.com/map3d/api/classes/Marker2D.html#togglePopup)|切换弹出信息框显示或隐藏|
|[setPopup](https://vjmap.com/map3d/api/classes/Marker2D.html#setPopup)|设置弹出信息框|
|[addTo](https://vjmap.com/map3d/api/classes/Marker2D.html#addTo)|增加至应用或实体中|
|[getElement](https://vjmap.com/map3d/api/classes/Marker2D.html#getElement)|获取元素对象|
|[getOpacity](https://vjmap.com/map3d/api/classes/Marker2D.html#getOpacity)|获取透明度（读元素 `style.opacity`，未设置时为 1）|
|[setOpacity](https://vjmap.com/map3d/api/classes/Marker2D.html#setOpacity)|设置透明度（写元素 `style.opacity`，有弹窗时一并设置）|
|[setAllowOverlap](https://vjmap.com/map3d/api/classes/Marker2D.html#setAllowOverlap)|设置是否允许重叠|
|[getBoundingRect](https://vjmap.com/map3d/api/classes/Marker2D.html#getBoundingRect)|获取边界范围（视口**屏幕像素**矩形，供碰撞检测用；不可见/尺寸为 0 时返回 `null`）|
|[isAllowOverlap](https://vjmap.com/map3d/api/classes/Marker2D.html#isAllowOverlap)|是否允许重叠|
|[setDraggable](https://vjmap.com/map3d/api/classes/Marker2D.html#setDraggable)|设置是否可拖动|
|[isDraggable](https://vjmap.com/map3d/api/classes/Marker2D.html#isDraggable)|是否可拖动|
|[remove](https://vjmap.com/map3d/api/classes/Marker2D.html#remove)|移除|
|[setAnimation](https://vjmap.com/map3d/api/classes/Marker2D.html#setAnimation)|设置动画|
|[setCursor](https://vjmap.com/map3d/api/classes/Marker2D.html#setCursor)|设置光标|

## 信号名

|方法|描述|
|:---|:---|
|dragstart|开始拖动|
|drag|拖动|
|dragend|结束拖动|
|click|点击|
|occlusion|遮挡状态变化，回调参数 `{object, occlusion}`。⚠️ 只有构造选项 `occlusionOpacity < 1`（默认 1）时才做遮挡检测，否则永不触发|
|elementSizeReady|元素大小已就绪。⚠️ 未设置 `offset`（或为 `[0,0]`）时在**构造函数内同步**派发，`new` 之后再 `.add` 的监听永远收不到；只有设置了非零 `offset` 才会在挂载后量到尺寸时异步派发|
|onAdd|增加|
|onRemove|移除|

对于div的事件，可以通过[getElement](https://vjmap.com/map3d/api/classes/Marker2D.html#getElement)获取div元素，对此div增加事件。
如
```js
marker2d.getElement().addEventListener("dblclick", event => {
    console.log("双击")
});
```

## 用法示例

### 创建一个marker2D
```js
let marker2d = new vjmap3d.Marker2D({
    color: vjmap3d.randHtmlColor(),
    draggable: true,
    allowOverlap: false
});
marker2d.setPosition([2, 0, 1]);
marker2d.addTo(app);
```

### 事件
```js
marker2d.signal.dragstart.add(() => {
    app.logInfo("dragstart");
});
marker2d.signal.drag.add(() => {
    app.logInfo("drag");
});
marker2d.signal.dragend.add(() => {
    app.logInfo("dragend");
});
marker2d.signal.click.add(() => {
    app.logInfo("click");
});
marker2d.signal.occlusion.add(() => { // 需构造时设 occlusionOpacity < 1 才会触发
    app.logInfo("occlusion");
});
marker2d.signal.elementSizeReady.add(() => { // ⚠️ 本例未设 offset，该信号已在构造时同步派发，这里收不到
    app.logInfo("elementSizeReady");
});
marker2d.signal.onAdd.add(() => {
    app.logInfo("onAdd");
});
marker2d.signal.onRemove.add(() => {
    app.logInfo("onRemove");
});
marker2d.getElement().addEventListener("contextmenu", event => {
    // @ts-ignore
    if (event.button != 2) return;//不是右键
    // 阻止地图默认右键菜单触发
    event.preventDefault();
    event.stopPropagation();
    new vjmap3d.ContextMenu({
        event: event,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `&lt;span style="color: #ffff00; padding-left: 30px">我是标注Marker的右键菜单&lt;/span>`},
            {
                label: '删除Marker',
                onClick: () => {
                    marker2d.remove()
                }
            }
        ]
    });
})
```

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let marker2d = new vjmap3d.Marker2D({
    color: vjmap3d.randHtmlColor(),
    draggable: true,
    allowOverlap: false
});
marker2d.setPosition([2, 0, 1]);
marker2d.addTo(app);
marker2d.signal.dragstart.add(() => {
    app.logInfo("dragstart");
});
marker2d.signal.drag.add(() => {
    app.logInfo("drag");
});
marker2d.signal.dragend.add(() => {
    app.logInfo("dragend");
});
marker2d.signal.click.add(() => {
    app.logInfo("click");
});
marker2d.signal.occlusion.add(() => { // 需构造时设 occlusionOpacity < 1 才会触发
    app.logInfo("occlusion");
});
marker2d.signal.elementSizeReady.add(() => { // ⚠️ 本例未设 offset，该信号已在构造时同步派发，这里收不到
    app.logInfo("elementSizeReady");
});
marker2d.signal.onAdd.add(() => {
    app.logInfo("onAdd");
});
marker2d.signal.onRemove.add(() => {
    app.logInfo("onRemove");
});
marker2d.getElement().addEventListener("contextmenu", event => {
    // @ts-ignore
    if (event.button != 2) return;//不是右键
    // 阻止地图默认右键菜单触发
    event.preventDefault();
    event.stopPropagation();
    new vjmap3d.ContextMenu({
        event: event,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `&lt;span style="color: #ffff00; padding-left: 30px">我是标注Marker的右键菜单&lt;/span>`},
            {
                label: '删除Marker',
                onClick: () => {
                    marker2d.remove()
                }
            }
        ]
    });
})
</pre>
</mapFrame>

### 关联信息提示框
```js
for (let i = 0; i < 100; i++) {
    let marker2d = new vjmap3d.Marker2D({
        color: vjmap3d.randHtmlColor(),
        allowOverlap: true,
        fadeDistance: 200,
        occlusionOpacity: 0.2
    });
    marker2d.setPosition(vjmap3d.toVector3(vjmap3d.randPoint3D()));
    marker2d.addTo(app);
    marker2d.setPopup(
        new vjmap3d.Popup2D({
            backgroundColor: "#A0FFA0",
            buttonColor: "#f00",
            closeOnClick: true,
            closeButton: true
        }).setHTML(`index${i + 1}`)
    );
    // marker2d.togglePopup();
}
```

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
for (let i = 0; i < 100; i++) {
    let marker2d = new vjmap3d.Marker2D({
        color: vjmap3d.randHtmlColor(),
        allowOverlap: true,
        fadeDistance: 200,
        occlusionOpacity: 0.2
    });
    marker2d.setPosition(vjmap3d.toVector3(vjmap3d.randPoint3D()));
    marker2d.addTo(app);
    marker2d.setPopup(
        new vjmap3d.Popup2D({
            backgroundColor: "#A0FFA0",
            buttonColor: "#f00",
            closeOnClick: true,
            closeButton: true
        }).setHTML(`index${i + 1}`)
    );
    // marker2d.togglePopup();
}
</pre>
</mapFrame>

### 遮挡时设置透明度
```js
let box = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshStandardMaterial({
    color: '#0ff'
});
let mesh = new THREE.Mesh(box, material);
mesh.position.set(0, 0, 0)
app.scene.add(mesh);
//
let marker2d = new vjmap3d.Marker2D({
    color: "#f00",
    occlusionOpacity: 0.2 // 遮挡时透明度
});
marker2d.setPosition([0, 0, -1.5]);
marker2d.addTo(app);  
marker2d.signal.occlusion.add(e => {
    if (e.occlusion) {
        app.logInfo("被遮挡", "warn");
    } else {
        app.logInfo("没被遮挡", "success");
    }
});
app.signal.onAppAfterRender.add(t => {
    app.cameraControl.azimuthAngle += 20 * t.deltaTime * THREE.MathUtils.DEG2RAD;
})
```
<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let box = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshStandardMaterial({
    color: '#0ff'
});
let mesh = new THREE.Mesh(box, material);
mesh.position.set(0, 0, 0)
app.scene.add(mesh);
//
let marker2d = new vjmap3d.Marker2D({
    color: "#f00",
    occlusionOpacity:0.2 // 遮挡时透明度
});
marker2d.setPosition([0, 0, -1.5]);
marker2d.addTo(app);  
marker2d.signal.occlusion.add(e => {
    if (e.occlusion) {
        app.logInfo("被遮挡", "warn");
    } else {
        app.logInfo("没被遮挡", "success");
    }
});
app.signal.onAppAfterRender.add(t => {
    app.cameraControl.azimuthAngle += 20 * t.deltaTime * THREE.MathUtils.DEG2RAD;
})
</pre>
</mapFrame>

### 动画Marker

```js
let marker2d = new vjmap3d.Marker2D({
    color: vjmap3d.randHtmlColor()
});
marker2d.setPosition(vjmap3d.toVector3(vjmap3d.toVector3(vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5}))))
marker2d.addTo(app);
marker2d.setAnimation("MAP_ANIMATION_BOUNCE");
```

### RotatingTextBorderMarker2d
```js
// 创建一个缺省的
let markerEle = new vjmap3d.RotatingTextBorderMarker(
    {
        position: vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5}),
        text: "3D地图"
    },
    {}
);
let marker2d = markerEle.createMarker2d({
    draggable: true
});
marker2d.addTo(app);
```

### BreathingApertureMarker
```js
// 创建一个缺省的
let markerEle = new vjmap3d.BreathingApertureMarker(
    {
        position: vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5}),
        text: "3D地图"
    },
    {
        textColor: vjmap3d.randHtmlColor()
    }
);
let marker2d = markerEle.createMarker2d({
    draggable: true
});
marker2d.addTo(app);
```

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
const ui = await app.getConfigPane({ title: "Marker", style: { width: "250px"}});
let gui = new vjmap3d.UiPanelJsonConfig();
gui.addButton("动画Marker", async () => {
    let marker2d = new vjmap3d.Marker2D({
        color: vjmap3d.randHtmlColor()
    });
    marker2d.setPosition(vjmap3d.toVector3(vjmap3d.toVector3(vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5}))))
    marker2d.addTo(app);
    marker2d.setAnimation("MAP_ANIMATION_BOUNCE");
});
gui.addButton("RotatingTextBorderMarker2d", async () => {
    // 创建一个缺省的
    let markerEle = new vjmap3d.RotatingTextBorderMarker(
        {
            position: vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5}),
            text: "3D地图"
        },
        {}
    );
    let marker2d = markerEle.createMarker2d({
        draggable: true
    });
    marker2d.addTo(app);
});
gui.addButton("BreathingApertureMarker", async () => {
    // 创建一个缺省的
    let markerEle = new vjmap3d.BreathingApertureMarker(
        {
            position: vjmap3d.randPoint3D({maxX: 5, maxY: 2, maxZ: 5, minX: -5, minY: -2, minZ: -5}),
            text: "3D地图"
        },
        {
            textColor: vjmap3d.randHtmlColor()
        }
    );
    let marker2d = markerEle.createMarker2d({
        draggable: true
    });
    marker2d.addTo(app);
});
gui.toJson().forEach(c => ui.appendChild(c));
</pre>
</mapFrame>

## 常见坑

1. **DOM 不是 addTo 同步挂载**：Marker2D 是 CSS2DObject，元素要等**下一渲染帧**由 CSS2DRenderer 挂进 DOM——`addTo(app)` 后立即断言 `getElement().isConnected` 为 false 是正常的，等一帧再查。
2. **被遮挡不会隐藏**：默认（`occlusionOpacity` = 1）不做遮挡检测，被实体挡住也照常显示；设置 `occlusionOpacity < 1` 后，被挡住时只是把透明度设为该值（并派发 `occlusion` 信号），元素**不会** `display:none`、也不会从 DOM 摘除。`display:none` 只发生在标记位于视锥外（如相机背后）或 `visible=false` 时。
3. **`getElement()` 返回的 div 可直接 addEventListener**（如 dblclick），见上文示例。
4. **`getBoundingRect()` 返回视口像素矩形**（来自元素的 `getBoundingClientRect()`，默认还并入弹窗范围）：`{x1,y1,x2,y2,cx,cy,rotation,position,zIndex,id,groupName,setVisible}`——没有 `width`/`height` 字段，尺寸取 `x2-x1`/`y2-y1`；透明度低于 `disableOpacity` 或元素尺寸为 0（未挂载）时返回 `null`。
5. **`signal.elementSizeReady` 通常收不到**：未设置非零 `offset` 时，它在构造函数里同步派发，`new` 返回后再挂的监听永远不会被调用；设置了 `offset` 时才依赖内部 ResizeObserver 首次量到非零尺寸后异步派发（headless/脱离视口场景可能不派发）。不要把它当"挂载完成"信号，用 `signal.onAdd` + 下一帧。
6. **`setOpacity` 同步写 DOM**：`getOpacity()` 读回与 `element.style.opacity` 一致。`setCursor('pointer')` 写 `element.style.cursor`。构造选项 `draggable`/`allowOverlap` 立即生效，`isDraggable()`/`isAllowOverlap()` 如实读回。
