|
@@ -21,11 +21,53 @@ function project([lon, lat], center, scale) {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function makeIconTexture(kind = 'meter', glyphOnly = false) {
|
|
|
|
|
+ const size = 64
|
|
|
|
|
+ const data = new Uint8Array(size * size * 4)
|
|
|
|
|
+ const put = (x, y, alpha = 255) => {
|
|
|
|
|
+ if (x < 0 || y < 0 || x >= size || y >= size) return
|
|
|
|
|
+ const i = (y * size + x) * 4
|
|
|
|
|
+ data[i] = data[i + 1] = data[i + 2] = 255
|
|
|
|
|
+ data[i + 3] = alpha
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!glyphOnly) {
|
|
|
|
|
+ for (let y = 3; y < 61; y++) for (let x = 3; x < 61; x++) {
|
|
|
|
|
+ const d = Math.hypot(x - 32, y - 32)
|
|
|
|
|
+ if (d < 29) put(x, y, d > 24 ? 255 : 210)
|
|
|
|
|
+ }
|
|
|
|
|
+ for (let y = 14; y <= 49; y++) for (let x = 14; x <= 49; x++) {
|
|
|
|
|
+ const d = Math.hypot(x - 32, y - 32)
|
|
|
|
|
+ if (d >= 13 && d <= 16) put(x, y)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const bars = kind === 'flow' ? [[22, 37, 27], [29, 34, 36], [36, 30, 42], [43, 25, 45]]
|
|
|
|
|
+ : kind === 'pressure' ? [[22, 36, 43], [29, 31, 43], [36, 26, 43], [43, 21, 43]]
|
|
|
|
|
+ : [[22, 33, 43], [29, 29, 43], [36, 34, 43], [43, 27, 43]]
|
|
|
|
|
+ for (const [x, top, bottom] of bars) for (let y = top; y <= bottom; y++) for (let dx = -1; dx <= 1; dx++) put(x + dx, y)
|
|
|
|
|
+ const texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat)
|
|
|
|
|
+ texture.colorSpace = THREE.SRGBColorSpace
|
|
|
|
|
+ texture.magFilter = THREE.LinearFilter
|
|
|
|
|
+ texture.minFilter = THREE.LinearMipmapLinearFilter
|
|
|
|
|
+ texture.generateMipmaps = true
|
|
|
|
|
+ texture.needsUpdate = true
|
|
|
|
|
+ return texture
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function metricIconKind(device) {
|
|
|
|
|
+ const type = `${device.typeName || ''} ${device.name || ''}`
|
|
|
|
|
+ if (/流量|flow/i.test(type)) return 'flow'
|
|
|
|
|
+ if (/压力|pressure/i.test(type)) return 'pressure'
|
|
|
|
|
+ return 'meter'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function disposeObject(root) {
|
|
function disposeObject(root) {
|
|
|
const resources = new Set()
|
|
const resources = new Set()
|
|
|
root.traverse((node) => {
|
|
root.traverse((node) => {
|
|
|
if (node.geometry) resources.add(node.geometry)
|
|
if (node.geometry) resources.add(node.geometry)
|
|
|
- if (node.material) for (const value of [].concat(node.material)) resources.add(value)
|
|
|
|
|
|
|
+ if (node.material) for (const value of [].concat(node.material)) {
|
|
|
|
|
+ resources.add(value)
|
|
|
|
|
+ if (value.map) resources.add(value.map)
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
for (const resource of resources) resource.dispose()
|
|
for (const resource of resources) resource.dispose()
|
|
|
}
|
|
}
|
|
@@ -146,12 +188,22 @@ function buildNeighborhood(plan) {
|
|
|
emissive: color, emissiveIntensity: 0.28, depthTest: false, depthWrite: false }))
|
|
emissive: color, emissiveIntensity: 0.28, depthTest: false, depthWrite: false }))
|
|
|
body.name = 'device-body'
|
|
body.name = 'device-body'
|
|
|
group.add(body)
|
|
group.add(body)
|
|
|
- const indicator = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.07, 0.035), new THREE.MeshBasicMaterial({ color: '#edffff' }))
|
|
|
|
|
- indicator.position.set(0, 0.05, 0.16)
|
|
|
|
|
- indicator.material.depthTest = false
|
|
|
|
|
- indicator.material.depthWrite = false
|
|
|
|
|
- indicator.renderOrder = 22
|
|
|
|
|
- group.add(indicator)
|
|
|
|
|
|
|
+ const iconTexture = makeIconTexture(metricIconKind(device))
|
|
|
|
|
+ const icon = new THREE.Sprite(new THREE.SpriteMaterial({ map: iconTexture, color, transparent: true,
|
|
|
|
|
+ depthTest: false, depthWrite: false, sizeAttenuation: true }))
|
|
|
|
|
+ icon.name = 'device-icon'
|
|
|
|
|
+ icon.position.set(0, 0.72, 0)
|
|
|
|
|
+ icon.renderOrder = 30
|
|
|
|
|
+ icon.userData.device = device
|
|
|
|
|
+ group.add(icon)
|
|
|
|
|
+ const glyphTexture = makeIconTexture(metricIconKind(device), true)
|
|
|
|
|
+ const iconGlyph = new THREE.Sprite(new THREE.SpriteMaterial({ map: glyphTexture, color: '#ffffff', transparent: true,
|
|
|
|
|
+ depthTest: false, depthWrite: false, sizeAttenuation: true }))
|
|
|
|
|
+ iconGlyph.name = 'device-icon-glyph'
|
|
|
|
|
+ iconGlyph.position.copy(icon.position)
|
|
|
|
|
+ iconGlyph.renderOrder = 31
|
|
|
|
|
+ iconGlyph.scale.setScalar(0.68)
|
|
|
|
|
+ group.add(iconGlyph)
|
|
|
root.add(group)
|
|
root.add(group)
|
|
|
}
|
|
}
|
|
|
return root
|
|
return root
|
|
@@ -220,6 +272,16 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
)
|
|
)
|
|
|
root?.traverse((node) => {
|
|
root?.traverse((node) => {
|
|
|
if (node.name === 'selected-pipe-outline') node.material.uniforms.resolution.value.set(host.clientWidth || 320, host.clientHeight || 220)
|
|
if (node.name === 'selected-pipe-outline') node.material.uniforms.resolution.value.set(host.clientWidth || 320, host.clientHeight || 220)
|
|
|
|
|
+ if (node.name === 'device-icon') {
|
|
|
|
|
+ root.updateMatrixWorld(true)
|
|
|
|
|
+ const world = 2 * camera.position.distanceTo(node.getWorldPosition(new THREE.Vector3()))
|
|
|
|
|
+ * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * 48 / (host.clientHeight || 220)
|
|
|
|
|
+ const parentScale = Math.max(0.0001, node.parent.getWorldScale(new THREE.Vector3()).x)
|
|
|
|
|
+ node.scale.setScalar(world / parentScale)
|
|
|
|
|
+ } else if (node.name === 'device-icon-glyph') {
|
|
|
|
|
+ const badge = node.parent.getObjectByName('device-icon')
|
|
|
|
|
+ node.scale.copy(badge.scale).multiplyScalar(0.68)
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
camera.near = Math.max(0.0001, distance > sceneRadius * 1.05 ? distance - sceneRadius * 1.05 : distance / 1000)
|
|
camera.near = Math.max(0.0001, distance > sceneRadius * 1.05 ? distance - sceneRadius * 1.05 : distance / 1000)
|
|
|
camera.far = Math.max(camera.near + 1, distance + sceneRadius * 2)
|
|
camera.far = Math.max(camera.near + 1, distance + sceneRadius * 2)
|
|
@@ -264,7 +326,9 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
return raycaster.intersectObject(root, true)[0] || null
|
|
return raycaster.intersectObject(root, true)[0] || null
|
|
|
}
|
|
}
|
|
|
const detachPick = attachPointPick(canvas, ({ x, y }) => {
|
|
const detachPick = attachPointPick(canvas, ({ x, y }) => {
|
|
|
- let node = hitAt(x, y)?.object
|
|
|
|
|
|
|
+ const intersections = raycaster.intersectObject(root, true)
|
|
|
|
|
+ const hit = intersections.find((item) => item.object.name === 'device-icon' || item.object.name === 'device-icon-glyph') || intersections[0]
|
|
|
|
|
+ let node = hit?.object
|
|
|
while (node) {
|
|
while (node) {
|
|
|
if (node.userData.device) { onDevicePick(node.userData.device); return }
|
|
if (node.userData.device) { onDevicePick(node.userData.device); return }
|
|
|
if (node.userData.sourceKind) { onObjectPick({ objectKind: node.userData.sourceKind, code: node.userData.code }); return }
|
|
if (node.userData.sourceKind) { onObjectPick({ objectKind: node.userData.sourceKind, code: node.userData.code }); return }
|