|
@@ -1,6 +1,12 @@
|
|
|
-/** Three.js adapter for one selected pipe or facility component. */
|
|
|
|
|
|
|
+/** Three.js adapter for a selected object and its bounded local underground context. */
|
|
|
import * as THREE from 'three'
|
|
import * as THREE from 'three'
|
|
|
import { enhancementLabel } from '../domain/pipeEnhancement.js'
|
|
import { enhancementLabel } from '../domain/pipeEnhancement.js'
|
|
|
|
|
+import { PIPE_PALETTE } from '../domain/pipeVisual.js'
|
|
|
|
|
+import { attachSceneFocus } from './sceneInputFocus.js'
|
|
|
|
|
+import { addDetailedPipeSegment } from './pipeDetailGeometry.js'
|
|
|
|
|
+import { attachPointPick } from './pointPick.js'
|
|
|
|
|
+import { pipeSurfaceMaterial, outlineSelectedPipe } from './pipeSurfaceMaterial.js'
|
|
|
|
|
+import { sourceDiameterM, pipeSurface } from '../domain/pipePhysical.js'
|
|
|
|
|
|
|
|
function defaultRendererFactory({ canvas }) {
|
|
function defaultRendererFactory({ canvas }) {
|
|
|
return new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true })
|
|
return new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true })
|
|
@@ -15,54 +21,75 @@ function project([lon, lat], center, scale) {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function bounds(path, center) {
|
|
|
|
|
- let extent = 1
|
|
|
|
|
- for (const point of path) {
|
|
|
|
|
- const x = Math.abs((point[0] - center[0]) * 111320)
|
|
|
|
|
- const z = Math.abs((point[1] - center[1]) * 111320)
|
|
|
|
|
- extent = Math.max(extent, x, z)
|
|
|
|
|
- }
|
|
|
|
|
- return Math.min(0.025, 7 / extent)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function disposeObject(root) {
|
|
function disposeObject(root) {
|
|
|
|
|
+ const resources = new Set()
|
|
|
root.traverse((node) => {
|
|
root.traverse((node) => {
|
|
|
- if (node.geometry) node.geometry.dispose()
|
|
|
|
|
- if (node.material) {
|
|
|
|
|
- const materials = Array.isArray(node.material) ? node.material : [node.material]
|
|
|
|
|
- materials.forEach((material) => material.dispose())
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (node.geometry) resources.add(node.geometry)
|
|
|
|
|
+ if (node.material) for (const value of [].concat(node.material)) resources.add(value)
|
|
|
})
|
|
})
|
|
|
|
|
+ for (const resource of resources) resource.dispose()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function material(color, opacity = 1) {
|
|
function material(color, opacity = 1) {
|
|
|
return new THREE.MeshStandardMaterial({ color, roughness: 0.62, metalness: 0.18, transparent: opacity < 1, opacity })
|
|
return new THREE.MeshStandardMaterial({ color, roughness: 0.62, metalness: 0.18, transparent: opacity < 1, opacity })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function addSegment(root, start, end, radius, color) {
|
|
|
|
|
- const delta = new THREE.Vector3().subVectors(end, start)
|
|
|
|
|
- const length = delta.length()
|
|
|
|
|
- if (!Number.isFinite(length) || length < 0.001) return
|
|
|
|
|
- const mesh = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, length, 16), material(color))
|
|
|
|
|
- mesh.position.copy(start).add(end).multiplyScalar(0.5)
|
|
|
|
|
- mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize())
|
|
|
|
|
- root.add(mesh)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function buildObject(plan) {
|
|
|
|
|
|
|
+function buildObject(plan, frame = null) {
|
|
|
const object = plan.object
|
|
const object = plan.object
|
|
|
const root = new THREE.Group()
|
|
const root = new THREE.Group()
|
|
|
root.name = `pipe-enhancement:${object.code}`
|
|
root.name = `pipe-enhancement:${object.code}`
|
|
|
- const center = object.position || object.path[0]
|
|
|
|
|
- const scale = bounds(object.path, center)
|
|
|
|
|
- const points = object.path.map((point) => project(point, center, scale))
|
|
|
|
|
- const color = object.industry === 'WATER' ? 0x3b9fe8 : object.industry === 'DRAINAGE' ? 0x41c7a2 : 0xff6e54
|
|
|
|
|
|
|
+ const center = object.renderKind === 'pipe'
|
|
|
|
|
+ ? [0, 1].map((axis) => {
|
|
|
|
|
+ const [min, max] = object.path.reduce(([lo, hi], point) =>
|
|
|
|
|
+ [Math.min(lo, point[axis]), Math.max(hi, point[axis])], [Infinity, -Infinity])
|
|
|
|
|
+ return (min + max) / 2
|
|
|
|
|
+ })
|
|
|
|
|
+ : object.position || object.path[0]
|
|
|
|
|
+ const scale = 1
|
|
|
|
|
+ const points = object.path.map((point) => project(point, frame?.center || center, scale))
|
|
|
|
|
+ const color = (PIPE_PALETTE[object.colorKey] || PIPE_PALETTE.UNKNOWN).color
|
|
|
|
|
|
|
|
if (object.renderKind === 'pipe') {
|
|
if (object.renderKind === 'pipe') {
|
|
|
- for (let i = 1; i < points.length; i += 1) addSegment(root, points[i - 1], points[i], 0.23, color)
|
|
|
|
|
- const ring = new THREE.Mesh(new THREE.TorusGeometry(0.31, 0.055, 10, 24), material(0xf4fbff, 0.82))
|
|
|
|
|
- ring.rotation.x = Math.PI / 2
|
|
|
|
|
- root.add(ring)
|
|
|
|
|
|
|
+ const diameterM = object.diameterM ?? sourceDiameterM(object.diameter)
|
|
|
|
|
+ const surface = object.surface || pipeSurface(object.material)
|
|
|
|
|
+ if (diameterM === null) {
|
|
|
|
|
+ const guide = new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), new THREE.LineDashedMaterial({ color, dashSize: 1, gapSize: 0.6 }))
|
|
|
|
|
+ guide.name = 'missing-diameter-guide'
|
|
|
|
|
+ guide.computeLineDistances()
|
|
|
|
|
+ root.add(guide)
|
|
|
|
|
+ }
|
|
|
|
|
+ for (let i = 1; diameterM !== null && i < points.length; i += 1) {
|
|
|
|
|
+ if (object.primary === false) {
|
|
|
|
|
+ const delta = new THREE.Vector3().subVectors(points[i], points[i - 1])
|
|
|
|
|
+ const length = delta.length()
|
|
|
|
|
+ if (length < 0.000001) continue
|
|
|
|
|
+ const mesh = new THREE.Mesh(new THREE.CylinderGeometry(diameterM / 2, diameterM / 2, length, 16, 1, true), pipeSurfaceMaterial(color, surface))
|
|
|
|
|
+ mesh.name = 'neighbor-pipe-wall'
|
|
|
|
|
+ mesh.position.copy(points[i - 1]).add(points[i]).multiplyScalar(0.5)
|
|
|
|
|
+ mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), delta.normalize())
|
|
|
|
|
+ root.add(mesh)
|
|
|
|
|
+ } else addDetailedPipeSegment(root, points[i - 1], points[i], color, {
|
|
|
|
|
+ startFlange: i === 1,
|
|
|
|
|
+ endFlange: i === points.length - 1,
|
|
|
|
|
+ radius: diameterM / 2,
|
|
|
|
|
+ surface,
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (object.renderKind === 'manhole') {
|
|
|
|
|
+ const shell = new THREE.Mesh(new THREE.CylinderGeometry(0.85, 0.85, 2.4, 32, 1, true, Math.PI * 0.25, Math.PI * 1.5),
|
|
|
|
|
+ new THREE.MeshStandardMaterial({ color: '#718093', roughness: 0.9, side: THREE.DoubleSide }))
|
|
|
|
|
+ shell.name = 'manhole-shaft'
|
|
|
|
|
+ shell.position.copy(points[0])
|
|
|
|
|
+ shell.position.y = -0.9
|
|
|
|
|
+ root.add(shell)
|
|
|
|
|
+ const base = new THREE.Mesh(new THREE.CylinderGeometry(0.86, 0.86, 0.12, 32), material('#53616f'))
|
|
|
|
|
+ base.position.copy(points[0]); base.position.y = -2.15
|
|
|
|
|
+ root.add(base)
|
|
|
|
|
+ const cover = new THREE.Mesh(new THREE.CylinderGeometry(0.9, 0.9, 0.12, 32), material(color))
|
|
|
|
|
+ cover.name = 'manhole-cover'
|
|
|
|
|
+ cover.position.copy(points[0]); cover.position.y = 0.55
|
|
|
|
|
+ cover.rotation.x = -0.2
|
|
|
|
|
+ root.add(cover)
|
|
|
} else {
|
|
} else {
|
|
|
const shape = object.renderKind === 'valve'
|
|
const shape = object.renderKind === 'valve'
|
|
|
? new THREE.CylinderGeometry(0.48, 0.48, 0.16, 24)
|
|
? new THREE.CylinderGeometry(0.48, 0.48, 0.16, 24)
|
|
@@ -76,15 +103,52 @@ function buildObject(plan) {
|
|
|
if (object.renderKind === 'elbow') mesh.rotation.y = Math.PI / 2
|
|
if (object.renderKind === 'elbow') mesh.rotation.y = Math.PI / 2
|
|
|
root.add(mesh)
|
|
root.add(mesh)
|
|
|
}
|
|
}
|
|
|
- root.position.y = 0
|
|
|
|
|
|
|
+ if (object.primary !== false) outlineSelectedPipe(root)
|
|
|
|
|
+ root.position.y = frame ? ((frame.depthM || 0) - (object.depthM || 0)) * scale : 0
|
|
|
|
|
+ root.userData = { code: object.code, sourceKind: object.sourceKind, primary: object.primary !== false, clipped: object.clipped === true }
|
|
|
|
|
+ return root
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildNeighborhood(plan) {
|
|
|
|
|
+ if ((!plan.objects || plan.objects.length <= 1) && !plan.devices?.length) return buildObject(plan)
|
|
|
|
|
+ const objects = plan.objects || [{ ...plan.object, primary: true, depthM: 0 }]
|
|
|
|
|
+ const path = [...objects.flatMap((object) => object.path), ...(plan.devices || []).map((device) => device.position)]
|
|
|
|
|
+ const center = [0, 1].map((axis) => {
|
|
|
|
|
+ const [min, max] = path.reduce(([lo, hi], p) => [Math.min(lo, p[axis]), Math.max(hi, p[axis])], [Infinity, -Infinity])
|
|
|
|
|
+ return (min + max) / 2
|
|
|
|
|
+ })
|
|
|
|
|
+ const frame = { center, scale: 1, depthM: objects[0].depthM }
|
|
|
|
|
+ const root = new THREE.Group()
|
|
|
|
|
+ root.name = `pipe-enhancement:${plan.object.code}`
|
|
|
|
|
+ for (const object of objects) root.add(buildObject({ object }, frame))
|
|
|
|
|
+ for (const device of plan.devices || []) {
|
|
|
|
|
+ const group = new THREE.Group()
|
|
|
|
|
+ group.name = `device:${device.code}`
|
|
|
|
|
+ group.userData.device = device
|
|
|
|
|
+ group.position.copy(project(device.position, center, frame.scale))
|
|
|
|
|
+ group.position.y = ((frame.depthM || 0) - device.depthM) * frame.scale + 0.35
|
|
|
|
|
+ const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.25, 8), material('#b9c5d0'))
|
|
|
|
|
+ stem.position.y = -0.16
|
|
|
|
|
+ group.add(stem)
|
|
|
|
|
+ const color = (PIPE_PALETTE[device.colorKey] || PIPE_PALETTE.UNKNOWN).color
|
|
|
|
|
+ const shape = device.industry === 'GAS' ? new THREE.BoxGeometry(0.22, 0.32, 0.18)
|
|
|
|
|
+ : device.industry === 'WATER_SUPPLY' ? new THREE.CylinderGeometry(0.15, 0.15, 0.28, 12)
|
|
|
|
|
+ : device.industry === 'MANHOLE' ? new THREE.BoxGeometry(0.3, 0.1, 0.22)
|
|
|
|
|
+ : new THREE.ConeGeometry(0.17, 0.3, 12)
|
|
|
|
|
+ group.add(new THREE.Mesh(shape, material(color)))
|
|
|
|
|
+ root.add(group)
|
|
|
|
|
+ }
|
|
|
|
|
+ root.updateMatrixWorld(true)
|
|
|
|
|
+ const box = new THREE.Box3().setFromObject(root)
|
|
|
|
|
+ root.position.sub(box.getCenter(new THREE.Vector3()))
|
|
|
return root
|
|
return root
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Mount a single-object viewer. The optional rendererFactory is a system-boundary seam
|
|
|
|
|
|
|
+ * Mount a bounded local viewer. The optional rendererFactory is a system-boundary seam
|
|
|
* for offline tests; production uses WebGLRenderer.
|
|
* for offline tests; production uses WebGLRenderer.
|
|
|
*/
|
|
*/
|
|
|
-export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRendererFactory, pixelRatio = 1 } = {}) {
|
|
|
|
|
|
|
+export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRendererFactory, pixelRatio = 1, onDevicePick = () => {} } = {}) {
|
|
|
if (!host || !plan || plan.visible !== true || !plan.object) {
|
|
if (!host || !plan || plan.visible !== true || !plan.object) {
|
|
|
return { status: 'unavailable', sync: () => {}, destroy: () => {}, getState: () => ({ status: 'unavailable' }) }
|
|
return { status: 'unavailable', sync: () => {}, destroy: () => {}, getState: () => ({ status: 'unavailable' }) }
|
|
|
}
|
|
}
|
|
@@ -111,24 +175,37 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
|
|
|
|
|
let root = null
|
|
let root = null
|
|
|
let current = plan
|
|
let current = plan
|
|
|
|
|
+ const target = new THREE.Vector3()
|
|
|
let yaw = 0
|
|
let yaw = 0
|
|
|
- let pitch = -0.28
|
|
|
|
|
- let distance = 7.2
|
|
|
|
|
- let dragging = false
|
|
|
|
|
- let lastX = 0
|
|
|
|
|
- let lastY = 0
|
|
|
|
|
|
|
+ let pitch = Math.atan2(3.8, 7.2)
|
|
|
|
|
+ let distance = Math.hypot(3.8, 7.2)
|
|
|
|
|
+ let sceneRadius = 1
|
|
|
|
|
+ let drag = null
|
|
|
const canvas = renderer.domElement || host
|
|
const canvas = renderer.domElement || host
|
|
|
|
|
+ let destroyed = false
|
|
|
const resize = () => {
|
|
const resize = () => {
|
|
|
|
|
+ if (destroyed) return
|
|
|
const width = host.clientWidth || 320
|
|
const width = host.clientWidth || 320
|
|
|
const height = host.clientHeight || 220
|
|
const height = host.clientHeight || 220
|
|
|
camera.aspect = width / Math.max(1, height)
|
|
camera.aspect = width / Math.max(1, height)
|
|
|
camera.updateProjectionMatrix()
|
|
camera.updateProjectionMatrix()
|
|
|
renderer.setSize?.(width, height, false)
|
|
renderer.setSize?.(width, height, false)
|
|
|
- renderer.render?.(scene, camera)
|
|
|
|
|
|
|
+ render()
|
|
|
}
|
|
}
|
|
|
const render = () => {
|
|
const render = () => {
|
|
|
- camera.position.set(Math.sin(yaw) * distance, 3.8 + Math.sin(pitch) * 2, Math.cos(yaw) * distance)
|
|
|
|
|
- camera.lookAt(0, 0, 0)
|
|
|
|
|
|
|
+ const horizontal = Math.cos(pitch) * distance
|
|
|
|
|
+ camera.position.set(
|
|
|
|
|
+ target.x + Math.sin(yaw) * horizontal,
|
|
|
|
|
+ target.y + Math.sin(pitch) * distance,
|
|
|
|
|
+ target.z + Math.cos(yaw) * horizontal,
|
|
|
|
|
+ )
|
|
|
|
|
+ root?.traverse((node) => {
|
|
|
|
|
+ if (node.name === 'selected-pipe-outline') node.material.uniforms.resolution.value.set(host.clientWidth || 320, host.clientHeight || 220)
|
|
|
|
|
+ })
|
|
|
|
|
+ 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.updateProjectionMatrix()
|
|
|
|
|
+ camera.lookAt(target)
|
|
|
renderer.render?.(scene, camera)
|
|
renderer.render?.(scene, camera)
|
|
|
}
|
|
}
|
|
|
const sync = (next) => {
|
|
const sync = (next) => {
|
|
@@ -137,28 +214,86 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
scene.remove(root)
|
|
scene.remove(root)
|
|
|
disposeObject(root)
|
|
disposeObject(root)
|
|
|
}
|
|
}
|
|
|
- root = buildObject(next)
|
|
|
|
|
|
|
+ root = buildNeighborhood(next)
|
|
|
scene.add(root)
|
|
scene.add(root)
|
|
|
|
|
+ root.updateMatrixWorld(true)
|
|
|
|
|
+ const box = new THREE.Box3().setFromObject(root)
|
|
|
|
|
+ const sphere = box.getBoundingSphere(new THREE.Sphere())
|
|
|
|
|
+ if (!box.isEmpty()) {
|
|
|
|
|
+ sceneRadius = Math.max(0.01, sphere.radius)
|
|
|
|
|
+ target.copy(sphere.center)
|
|
|
|
|
+ const halfFov = Math.atan(Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * Math.min(1, camera.aspect))
|
|
|
|
|
+ distance = sceneRadius / Math.sin(halfFov) * 1.12
|
|
|
|
|
+ camera.near = Math.max(0.0001, sceneRadius / 10000)
|
|
|
|
|
+ camera.far = Math.max(100, sceneRadius * 100)
|
|
|
|
|
+ camera.updateProjectionMatrix()
|
|
|
|
|
+ }
|
|
|
render()
|
|
render()
|
|
|
}
|
|
}
|
|
|
- const onDown = (event) => { dragging = true; lastX = event.clientX; lastY = event.clientY }
|
|
|
|
|
|
|
+ const detachFocus = attachSceneFocus(host)
|
|
|
|
|
+ const raycaster = new THREE.Raycaster()
|
|
|
|
|
+ const detachPick = attachPointPick(canvas, ({ x, y }) => {
|
|
|
|
|
+ if (!root || !canvas.getBoundingClientRect) return
|
|
|
|
|
+ const rect = canvas.getBoundingClientRect()
|
|
|
|
|
+ if (!rect.width || !rect.height) return
|
|
|
|
|
+ camera.updateMatrixWorld()
|
|
|
|
|
+ root.updateMatrixWorld(true)
|
|
|
|
|
+ raycaster.setFromCamera(new THREE.Vector2((x - rect.left) / rect.width * 2 - 1, 1 - (y - rect.top) / rect.height * 2), camera)
|
|
|
|
|
+ const hit = raycaster.intersectObject(root, true)[0]
|
|
|
|
|
+ let node = hit?.object
|
|
|
|
|
+ while (node && !node.userData.device) node = node.parent
|
|
|
|
|
+ if (node?.userData.device) onDevicePick(node.userData.device)
|
|
|
|
|
+ })
|
|
|
|
|
+ const onDown = (event) => {
|
|
|
|
|
+ if (drag || (event.button !== 0 && event.button !== 2)) return
|
|
|
|
|
+ drag = { button: event.button, id: event.pointerId, x: event.clientX, y: event.clientY }
|
|
|
|
|
+ event.preventDefault?.()
|
|
|
|
|
+ try { canvas.setPointerCapture?.(event.pointerId) } catch { /* 合成事件可能没有可捕获指针。 */ }
|
|
|
|
|
+ }
|
|
|
const onMove = (event) => {
|
|
const onMove = (event) => {
|
|
|
- if (!dragging) return
|
|
|
|
|
- yaw += (event.clientX - lastX) * 0.01
|
|
|
|
|
- pitch = Math.max(-1.1, Math.min(1.1, pitch + (event.clientY - lastY) * 0.008))
|
|
|
|
|
- lastX = event.clientX; lastY = event.clientY; render()
|
|
|
|
|
|
|
+ if (!drag || event.pointerId !== drag.id) return
|
|
|
|
|
+ const dx = event.clientX - drag.x
|
|
|
|
|
+ const dy = event.clientY - drag.y
|
|
|
|
|
+ if (drag.button === 0) {
|
|
|
|
|
+ // 将像素位移换算为目标平面距离,保持“抓住场景拖动”的方向和速度。
|
|
|
|
|
+ const unitsPerPixel = 2 * distance * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) / (host.clientHeight || 220)
|
|
|
|
|
+ const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion)
|
|
|
|
|
+ const up = new THREE.Vector3(0, 1, 0).applyQuaternion(camera.quaternion)
|
|
|
|
|
+ target.addScaledVector(right, -dx * unitsPerPixel).addScaledVector(up, dy * unitsPerPixel)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ yaw -= dx * 0.01
|
|
|
|
|
+ pitch = Math.max(-1.4, Math.min(1.4, pitch + dy * 0.008))
|
|
|
|
|
+ }
|
|
|
|
|
+ drag.x = event.clientX
|
|
|
|
|
+ drag.y = event.clientY
|
|
|
|
|
+ render()
|
|
|
|
|
+ }
|
|
|
|
|
+ const onUp = (event) => {
|
|
|
|
|
+ if (!drag || (event?.pointerId !== undefined && event.pointerId !== drag.id)) return
|
|
|
|
|
+ const id = drag.id
|
|
|
|
|
+ drag = null
|
|
|
|
|
+ try { canvas.releasePointerCapture?.(id) } catch { /* 指针可能已由浏览器释放。 */ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const onLeave = (event) => {
|
|
|
|
|
+ if (!canvas.hasPointerCapture?.(event.pointerId)) onUp(event)
|
|
|
|
|
+ }
|
|
|
|
|
+ const onWheel = (event) => {
|
|
|
|
|
+ event.preventDefault?.()
|
|
|
|
|
+ distance = Math.max(sceneRadius * 0.05, Math.min(sceneRadius * 50, distance * Math.exp(Math.max(-1000, Math.min(1000, event.deltaY)) * 0.001)))
|
|
|
|
|
+ render()
|
|
|
}
|
|
}
|
|
|
- const onUp = () => { dragging = false }
|
|
|
|
|
- const onWheel = (event) => { distance = Math.max(3.5, Math.min(12, distance + event.deltaY * 0.004)); render() }
|
|
|
|
|
canvas.addEventListener?.('pointerdown', onDown)
|
|
canvas.addEventListener?.('pointerdown', onDown)
|
|
|
canvas.addEventListener?.('pointermove', onMove)
|
|
canvas.addEventListener?.('pointermove', onMove)
|
|
|
canvas.addEventListener?.('pointerup', onUp)
|
|
canvas.addEventListener?.('pointerup', onUp)
|
|
|
- canvas.addEventListener?.('pointerleave', onUp)
|
|
|
|
|
- canvas.addEventListener?.('wheel', onWheel)
|
|
|
|
|
|
|
+ canvas.addEventListener?.('pointercancel', onUp)
|
|
|
|
|
+ canvas.addEventListener?.('lostpointercapture', onUp)
|
|
|
|
|
+ canvas.addEventListener?.('pointerleave', onLeave)
|
|
|
|
|
+ canvas.addEventListener?.('wheel', onWheel, { passive: false })
|
|
|
resize()
|
|
resize()
|
|
|
sync(plan)
|
|
sync(plan)
|
|
|
|
|
+ const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(resize) : null
|
|
|
|
|
+ observer?.observe(host)
|
|
|
|
|
|
|
|
- let destroyed = false
|
|
|
|
|
return {
|
|
return {
|
|
|
status: 'ready',
|
|
status: 'ready',
|
|
|
title: enhancementLabel(plan.object.sourceKind),
|
|
title: enhancementLabel(plan.object.sourceKind),
|
|
@@ -168,10 +303,16 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
destroy() {
|
|
destroy() {
|
|
|
if (destroyed) return
|
|
if (destroyed) return
|
|
|
destroyed = true
|
|
destroyed = true
|
|
|
|
|
+ onUp()
|
|
|
|
|
+ observer?.disconnect()
|
|
|
|
|
+ detachFocus()
|
|
|
|
|
+ detachPick()
|
|
|
canvas.removeEventListener?.('pointerdown', onDown)
|
|
canvas.removeEventListener?.('pointerdown', onDown)
|
|
|
canvas.removeEventListener?.('pointermove', onMove)
|
|
canvas.removeEventListener?.('pointermove', onMove)
|
|
|
canvas.removeEventListener?.('pointerup', onUp)
|
|
canvas.removeEventListener?.('pointerup', onUp)
|
|
|
- canvas.removeEventListener?.('pointerleave', onUp)
|
|
|
|
|
|
|
+ canvas.removeEventListener?.('pointercancel', onUp)
|
|
|
|
|
+ canvas.removeEventListener?.('lostpointercapture', onUp)
|
|
|
|
|
+ canvas.removeEventListener?.('pointerleave', onLeave)
|
|
|
canvas.removeEventListener?.('wheel', onWheel)
|
|
canvas.removeEventListener?.('wheel', onWheel)
|
|
|
if (root) disposeObject(root)
|
|
if (root) disposeObject(root)
|
|
|
renderer.dispose?.()
|
|
renderer.dispose?.()
|