|
|
@@ -5,6 +5,8 @@ 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 }) {
|
|
|
return new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true })
|
|
|
@@ -19,25 +21,13 @@ function project([lon, lat], center, scale) {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-function fitScale(path, center) {
|
|
|
- let extent = 0.01
|
|
|
- const cosLat = Math.cos((center[1] * Math.PI) / 180)
|
|
|
- for (const point of path) {
|
|
|
- const x = Math.abs((point[0] - center[0]) * 111320 * cosLat)
|
|
|
- const z = Math.abs((point[1] - center[1]) * 111320)
|
|
|
- extent = Math.max(extent, x, z)
|
|
|
- }
|
|
|
- return 2.5 / extent
|
|
|
-}
|
|
|
-
|
|
|
function disposeObject(root) {
|
|
|
+ const resources = new Set()
|
|
|
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) {
|
|
|
@@ -55,17 +45,25 @@ function buildObject(plan, frame = null) {
|
|
|
return (min + max) / 2
|
|
|
})
|
|
|
: object.position || object.path[0]
|
|
|
- const scale = frame?.scale ?? fitScale(object.path, center)
|
|
|
+ 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') {
|
|
|
- for (let i = 1; i < points.length; i += 1) {
|
|
|
+ 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.001) continue
|
|
|
- const mesh = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, length, 12, 1, true), material(color))
|
|
|
+ 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())
|
|
|
@@ -73,6 +71,8 @@ function buildObject(plan, frame = null) {
|
|
|
} 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') {
|
|
|
@@ -103,6 +103,7 @@ function buildObject(plan, frame = null) {
|
|
|
if (object.renderKind === 'elbow') mesh.rotation.y = Math.PI / 2
|
|
|
root.add(mesh)
|
|
|
}
|
|
|
+ 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
|
|
|
@@ -116,9 +117,7 @@ function buildNeighborhood(plan) {
|
|
|
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 depths = [...objects, ...(plan.devices || [])].map((item) => item.depthM || 0)
|
|
|
- const depthSpread = Math.max(5, ...depths.map((depth) => Math.abs(depth - depths[0])))
|
|
|
- const frame = { center, scale: Math.min(fitScale(path, center), 2.5 / depthSpread), depthM: objects[0].depthM }
|
|
|
+ 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))
|
|
|
@@ -180,6 +179,7 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
let yaw = 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
|
|
|
let destroyed = false
|
|
|
@@ -190,7 +190,7 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
camera.aspect = width / Math.max(1, height)
|
|
|
camera.updateProjectionMatrix()
|
|
|
renderer.setSize?.(width, height, false)
|
|
|
- renderer.render?.(scene, camera)
|
|
|
+ render()
|
|
|
}
|
|
|
const render = () => {
|
|
|
const horizontal = Math.cos(pitch) * distance
|
|
|
@@ -199,6 +199,12 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
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)
|
|
|
}
|
|
|
@@ -210,6 +216,18 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
}
|
|
|
root = buildNeighborhood(next)
|
|
|
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()
|
|
|
}
|
|
|
const detachFocus = attachSceneFocus(host)
|
|
|
@@ -261,7 +279,7 @@ export function mountPipeEnhancement(host, plan, { rendererFactory = defaultRend
|
|
|
}
|
|
|
const onWheel = (event) => {
|
|
|
event.preventDefault?.()
|
|
|
- distance = Math.max(3.5, Math.min(12, distance + event.deltaY * 0.004))
|
|
|
+ distance = Math.max(sceneRadius * 0.05, Math.min(sceneRadius * 50, distance * Math.exp(Math.max(-1000, Math.min(1000, event.deltaY)) * 0.001)))
|
|
|
render()
|
|
|
}
|
|
|
canvas.addEventListener?.('pointerdown', onDown)
|