Selaa lähdekoodia

Add PickingPlugin with example and utils - SelectionWidget, BoxSelectionWidget, ObjectPicker, IWidget

master
Palash Bansal 2 vuotta sitten
vanhempi
commit
6c1013d9ed
No account linked to committer's email address

+ 71
- 0
README.md Näytä tiedosto

@@ -91,6 +91,7 @@ To make changes and run the example, click on the CodePen button on the top righ
- [DepthBufferPlugin](#depthbufferplugin) - Pre-rendering of depth buffer
- [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer
- [GBufferPlugin](#depthnormalbufferplugin) - Pre-rendering of depth and normal buffers in a single pass buffer
- [PickingPlugin](#pickingplugin) - Adds support for selecting objects in the viewer with user interactions and selection widgets
- [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations
- [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening
- [RenderTargetPreviewPlugin](#rendertargetpreviewplugin) - Preview any render target in a UI panel over the canvas
@@ -2075,6 +2076,76 @@ const normalTarget = normalPlugin.target;

todo

## PickingPlugin

[//]: # (todo: image)

Example: https://threepipe.org/examples/#picking-plugin/

Source Code: [src/plugins/pipeline/PickingPlugin.ts](./src/plugins/interaction/PickingPlugin.ts)

API Reference: [PickingPlugin](https://threepipe.org/docs/classes/PickingPlugin.html)

Picking Plugin adds support for selecting and hovering over objects in the viewer with user interactions and selection widgets.

When the plugin is added to the viewer, it starts listening to the mouse move and click events over the canvas.
When an object is clicked, it is selected,
and if a UI plugin is added, the uiconfig for the selected object is populated in the interface.
The events `selectedObjectChanged`, `hoverObjectChanged`, and `hitObject` can be listened to on the plugin.

Picking plugin internally uses [ObjectPicker](https://threepipe.org/docs/classes/ObjectPicker.html),
check out the documentation or source code for more information.

```typescript
import {ThreeViewer, PickingPlugin} from 'threepipe'

const viewer = new ThreeViewer({...})

const pickingPlugin = viewer.addPluginSync(new PickingPlugin())

// Hovering events are also supported, but since its computationally expensive for large scenes it is disabled by default.
pickingPlugin.hoverEnabled = true

pickingPlugin.addEventListener('hitObject', (e)=>{
// This is fired when the user clicks on the canvas.
// The selected object hasn't been changed yet, and we have the option to change it or disable selection at this point.
// e.intersects.selectedObject contains the object that the user clicked on.
console.log('Hit: ', e.intersects.selectedObject)
// It can be changed here
// e.intersects.selectedObject = e.intersects.selectedObject.parent // select the parent
// e.intersects.selectedObject = null // unselect
// Check other properties on the event like intersects, mouse position, normal etc.
console.log(e)
})

pickingPlugin.addEventListener('selectedObjectChanged', (e)=>{
// This is fired when the selected object is changed.
// e.object contains the new selected object. It can be null if nothing is selected.
console.log('Selected: ', e.object)
})

// Objects can be programmatically selected and unselected

// to select
pickingPlugin.setSelectedObject(object)

// get the selected object
console.log(pickingPlugin.getSelectedObject())
// to unselect
pickingPlugin.setSelectedObject(null)

// Select object with camera animation to the object
pickingPlugin.setSelectedObject(object, true)

pickingPlugin.addEventListener('hoverObjectChanged', (e)=>{
// This is fired when the hovered object is changed.
// e.object contains the new hovered object.
console.log('Hovering: ', e.object)
})

```

## GLTFAnimationPlugin


+ 36
- 0
examples/picking-plugin/index.html Näytä tiedosto

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Picking Plugin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 32
- 0
examples/picking-plugin/script.ts Näytä tiedosto

@@ -0,0 +1,32 @@
import {_testFinish, IObject3D, PickingPlugin, ThreeViewer} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
})

const picking = viewer.addPluginSync(PickingPlugin)
picking.hoverEnabled = true

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf')

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))
ui.setupPluginUi(PickingPlugin)

picking.addEventListener('hitObject', (e)=>{
console.log('Hit object', e)
})
picking.addEventListener('selectedObjectChanged', (e)=>{
console.log('Selected Object Changed', e)
})

picking.addEventListener('hoverObjectChanged', (e)=>{
console.log('Hover Object Changed', e)
})

}

init().then(_testFinish)

+ 8
- 1
src/core/IObject.ts Näytä tiedosto

@@ -19,6 +19,7 @@ export interface IObject3DEvent<T extends string = IObject3DEventTypes> extends
oldMaterial?: IMaterial|undefined|IMaterial[] // from materialChanged
geometry?: IGeometry|undefined // from geometryUpdate, geometryChanged
oldGeometry?: IGeometry|undefined // from geometryChanged
source?: any
}

export interface ISetDirtyCommonOptions {
@@ -90,6 +91,11 @@ export interface IObject3DUserData extends IImportResultUserData {

license?: string

/**
* When false, this object will not be selectable when clicking on it.
*/
userSelectable?: boolean

// region root scene model root

/**
@@ -136,7 +142,7 @@ export interface IObject3DUserData extends IImportResultUserData {
}

export interface IObject3D<E extends Event = IObject3DEvent, ET = IObject3DEventTypes> extends Object3D<E, ET>, IUiConfigContainer, IDisposable {
assetType: 'model' | 'light' | 'camera'
assetType: 'model' | 'light' | 'camera' | 'widget'
isLight?: boolean
isCamera?: boolean
isMesh?: boolean
@@ -144,6 +150,7 @@ export interface IObject3D<E extends Event = IObject3DEvent, ET = IObject3DEvent
// isGroup?: boolean
isScene?: boolean
// isHelper?: boolean
isWidget?: boolean
readonly isObject3D: true

material?: IMaterial | IMaterial[]

+ 8
- 1
src/core/IScene.ts Näytä tiedosto

@@ -60,6 +60,8 @@ export type ISceneEventTypes = IObject3DEventTypes | 'sceneUpdate' | 'addSceneOb

export interface ISceneEvent<T extends string = ISceneEventTypes> extends IObject3DEvent<T> {
scene?: IScene | null

hierarchyChanged?: boolean // for 'sceneUpdate' event
// change?: string
}
export type ISceneSetDirtyOptions = IObjectSetDirtyOptions
@@ -67,7 +69,12 @@ export type ISceneSetDirtyOptions = IObjectSetDirtyOptions

export type ISceneUserData = IObject3DUserData

export type IWidget = IObject3D // todo
// todo improve
export interface IWidget {
attach(object: any): this;
detach(): this;
isWidget: true;
}

export interface IScene<E extends ISceneEvent = ISceneEvent, ET extends ISceneEventTypes = ISceneEventTypes>
extends Scene<E, ET>, IObject3D<E, ET>, IShaderPropertiesUpdater {

+ 2
- 2
src/core/object/IObjectUi.ts Näytä tiedosto

@@ -68,7 +68,7 @@ export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjec
label: 'Pick/Focus',
value: ()=>{
// todo instead of dispatching, make a IObject3D.select function
this.dispatchEvent({type: 'select', ui: true, value: this, bubbleToParent: true, focusCamera: true})
this.dispatchEvent({type: 'select', ui: true, object: this, bubbleToParent: true, focusCamera: true})
},
},
{
@@ -78,7 +78,7 @@ export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjec
value: ()=>{
const parent = this.parent
if (parent) {
parent.dispatchEvent({type: 'select', ui: true, bubbleToParent: true, value: parent})
parent.dispatchEvent({type: 'select', ui: true, bubbleToParent: true, object: parent})
}
},
},

+ 1
- 0
src/core/object/iObjectCommons.ts Näytä tiedosto

@@ -343,6 +343,7 @@ function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectPr

if (this.isLight) this.assetType = 'light'
else if (this.isCamera) this.assetType = 'camera'
else if (this.isWidget) this.assetType = 'widget'
else this.assetType = 'model'

if (parent) this.parentRoot = parent

+ 1
- 0
src/plugins/index.ts Näytä tiedosto

@@ -20,6 +20,7 @@ export {SceneUiConfigPlugin} from './ui/SceneUiConfigPlugin'
// interaction
export {DropzonePlugin, type DropzonePluginOptions} from './interaction/DropzonePlugin'
export {FullScreenPlugin} from './interaction/FullScreenPlugin'
export {PickingPlugin} from './interaction/PickingPlugin'

// import
export {Rhino3dmLoadPlugin} from './import/Rhino3dmLoadPlugin'

+ 248
- 0
src/plugins/interaction/PickingPlugin.ts Näytä tiedosto

@@ -0,0 +1,248 @@
import {Object3D} from 'three'
import {Class, serialize} from 'ts-browser-helpers'
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {ObjectPicker} from '../../three/utils/ObjectPicker'
import {IObject3D, IObject3DEvent, ISceneEvent} from '../../core'
import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js'
import {BoxSelectionWidget, SelectionWidget} from '../../three/utils/SelectionWidget'

export class PickingPlugin extends AViewerPluginSync<'selectedObjectChanged'|'hoverObjectChanged'|'hitObject'> {
@serialize() enabled = true
private _enableWidget = true

get picker(): ObjectPicker|undefined {
return this._picker
}
static readonly PluginType = 'Picking'
private _picker?: ObjectPicker
private _widget?: SelectionWidget
private _pickUi: boolean

get hoverEnabled() {
return this._picker?.hoverEnabled ?? false
}
set hoverEnabled(v: boolean) {
if (!this._picker) return
this._picker.hoverEnabled = v
this.uiConfig && this.uiConfig.uiRefresh?.()
}

@serialize()
autoFocus = false

public setDirty() {
this._viewer?.setDirty()
}
constructor(selection: Class<SelectionWidget>|undefined = BoxSelectionWidget, pickUi = true, autoFocus = false) {
super()
if (selection) {
this._widget = new selection()
}
this._pickUi = pickUi
this.autoFocus = autoFocus
this.dispatchEvent = this.dispatchEvent.bind(this)
}

getSelectedObject<T extends IObject3D = IObject3D>(): T|undefined {
if (!this.enabled) return
return this._picker?.selectedObject as T || undefined
}

setSelectedObject(object: IObject3D|undefined, focusCamera = false) { // todo: listen to object dispose
if (!this.enabled) return
if (!this._picker) return
const t = this.autoFocus
this.autoFocus = false
this._picker.selectedObject = object || null
this.autoFocus = t
if (t || focusCamera) this.focusObject(object)
}

onAdded(viewer: ThreeViewer): void {
super.onAdded(viewer)
this._picker = new ObjectPicker(viewer.scene.modelRoot, viewer.canvas, viewer.scene.mainCamera, (obj)=>{
const hasMat = obj.material
if (!hasMat) return false
let o: IObject3D|null = obj
let ret = false
while (o) {
if (!o.visible) return false
if (o.assetType === 'model' || o.assetType === 'light') ret = true
if (o.assetType === 'widget') return false
if (o.userData.userSelectable === false) return false
if (o.userData.bboxVisible === false) return false
o = o.parent
}
return ret
})
if (this._widget) viewer.scene.addObject(this._widget, {addToRoot: true})

this._picker.addEventListener('selectedObjectChanged', this._selectedObjectChanged)
this._picker.addEventListener('hoverObjectChanged', this.dispatchEvent)
this._picker.addEventListener('hitObject', this._onObjectHit)

// on material drop on selected object
// viewer.scene.addEventListener('addSceneObject', async(e) => {
// const obj = e.object
// const selected: IModel<Mesh> = this.getSelectedObject()! as any
// if (selected
// && obj?.assetType === 'material'
// && typeof selected?.setMaterial === 'function'
// && selected?.modelObject?.isMesh
// && await viewer.confirm('Applying material: Apply material to the selected object?')
// ) {
// const oldMat = selected.material
// if (Array.isArray(oldMat)) {
// console.warn('Dropping on material array not yet fully supported.')
// selected.setMaterial(obj)
// } else {
// let meshes: IModel<Mesh>[] = Array.from(oldMat?.userData.__appliedMeshes ?? [])
// const c = meshes.length > 1 ? !await viewer.confirm('Applying material: Apply to all objects using this material?') : meshes.length < 1
// if (c) meshes = [selected]
// for (const mesh of meshes) {
// if (mesh) mesh.setMaterial?.(obj)
// }
// }
// }
// })
viewer.scene.addEventListener('select', this._onObjectSelectEvent)
viewer.scene.addEventListener('sceneUpdate', this._onSceneUpdate)
viewer.scene.addEventListener('mainCameraChange', this._mainCameraChange)

}

onRemove(viewer: ThreeViewer) {
viewer.scene.removeEventListener('select', this._onObjectSelectEvent)
viewer.scene.removeEventListener('sceneUpdate', this._onSceneUpdate)
viewer.scene.removeEventListener('mainCameraChange', this._mainCameraChange)

this._widget?.removeFromParent()

if (this._picker) {
this._picker.removeEventListener('selectedObjectChanged', this._selectedObjectChanged)
this._picker.removeEventListener('hoverObjectChanged', this.dispatchEvent)
this._picker.removeEventListener('hitObject', this._onObjectHit)
this._picker.dispose()
this._picker = undefined
}
super.onRemove(viewer)
}

dispose() {
super.dispose()
this._widget?.dispose()
}

private _mainCameraChange = ()=>{
if (!this._picker || !this._viewer) return
this._picker.camera = this._viewer.scene.mainCamera
}
private _onSceneUpdate = (e: ISceneEvent)=>{
if (!e.hierarchyChanged) return
const s = this.getSelectedObject()
let inScene = false
s?.traverseAncestors((o)=>{
if (o === this._viewer?.scene) inScene = true
})
if (!inScene) this.setSelectedObject(undefined)
}

private _onObjectSelectEvent = (e: IObject3DEvent)=>{
if (e.source === PickingPlugin.PluginType) return
if (e.object === undefined && e.value === undefined) console.error('e.object or e.value must be set for picking, can be null to unselect')
else this.setSelectedObject(e.value, this.autoFocus || e.focusCamera)
}

private _selectedObjectChanged = (e: any) => {
this.dispatchEvent(e)
const selected = this._picker?.selectedObject || undefined

if (this._pickUi) {
const sUiConfig = (selected as IUiConfigContainer)?.uiConfig
const ui = this.uiConfig
ui.children = [...this._uiConfigChildren]
if (sUiConfig) ui.children.push(sUiConfig)
ui.uiRefresh?.()
}

const widget = this._widget
if (widget && this._enableWidget) {
if (selected) widget.attach(selected)
else widget.detach()
}

// if (selected) selected.dispatchEvent({type: 'selected', source: PickingPlugin.PluginType, object: selected})

this._viewer?.setDirty()

if (this.autoFocus) {
// this._viewer?.resetCamera({rootObject: selected, centerOffset: new Vector3(4, 4, 4)})
this.focusObject(selected)
}


}

private _onObjectHit = (e: any)=>{
if (!this._viewer) return
if (!this.enabled) {
e.intersects.selectedObject = null
return
}
this.dispatchEvent(e)
}

// @ts-expect-error temporary
public async focusObject(selected?: Object3D) {
// const camViews = this._viewer?.getPluginByType<CameraViewPlugin>('CameraViews')
// await camViews?.animateToFitObject(selected, 1.25, 1000, 'easeOut', {min: (this._viewer?.scene.activeCamera.getControls<OrbitControls3>()?.minDistance ?? 0.5) + 0.5, max: 50.0})
}

public enableWidget(enable: boolean): void {
this._enableWidget = enable
if (enable) {
const selected = this._picker?.selectedObject || undefined
if (selected)
this._widget?.attach(selected)
} else {
this._widget?.detach()
}
}

private _uiConfigChildren: UiObjectConfig[] = [
{
label: 'Enabled',
type: 'checkbox',
property: [this, 'enabled'],
},
{
label: 'Hover Enabled',
type: 'checkbox',
property: [this, 'hoverEnabled'],
},
{
label: 'AutoFocus',
type: 'checkbox',
property: [this, 'autoFocus'],
onChange: ()=>{
const o = this.getSelectedObject()
if (this.autoFocus && o) this.setSelectedObject(o, true)
},
},
]

uiConfig: UiObjectConfig = {
type: 'panel',
label: 'Picker',
expanded: true,
children: [
...this._uiConfigChildren,
],
}

get widget(): SelectionWidget | undefined {
return this._widget
}

}


+ 262
- 0
src/three/utils/ObjectPicker.ts Näytä tiedosto

@@ -0,0 +1,262 @@
import {Event, EventDispatcher, Intersection, Raycaster, Vector2} from 'three'
import {now} from 'ts-browser-helpers'
import {ICamera, IObject3D} from '../../core'

export class ObjectPicker extends EventDispatcher<Event, 'hoverObjectChanged'|'selectedObjectChanged'|'hitObject'> {
private _firstHit: IObject3D | undefined

hoverEnabled = false

private _root: IObject3D
private _camera: ICamera
private _mouseDownTime: number
private _mouseUpTime: number
private _time: number
public selectionCondition: (o: IObject3D) => boolean
public raycaster: Raycaster
public mouse: Vector2
private _selected: IObject3D[]
private _hovering: IObject3D[]
public cursorStyles: {default: string; down: string}
public domElement: HTMLElement
constructor(root: IObject3D, domElement: HTMLElement, camera: ICamera, selectionCondition?: (o:IObject3D)=>boolean) {
super()
this._root = root
this._camera = camera
this.domElement = domElement

this._time = this.time
this._mouseDownTime = 0
this._mouseUpTime = 1

this.selectionCondition = selectionCondition ?? (
(selectedObject: any) => {
return selectedObject.userData.userSelectable !== false && selectedObject.userData.bboxVisible !== false && selectedObject.material != null && selectedObject.material.type !== 'ShadowMaterial' // sample to select only mesh with material and not shadowmaterial.
})

this.raycaster = new Raycaster()

this.mouse = new Vector2()
this._selected = []
this._hovering = []

this.cursorStyles = {
default: 'grab',
down: 'grabbing',
}

this.domElement.style.touchAction = 'none'
// this.domElement.style.cursor = this.cursorStyles.default
this.domElement.addEventListener('pointermove', this._onPointerMove)
this.domElement.addEventListener('pointerleave', this._onPointerLeave)
this.domElement.addEventListener('pointerout', this._onPointerLeave)
this.domElement.addEventListener('pointercancel', this._onPointerCancel)
this.domElement.addEventListener('pointerenter', this._onPointerEnter)
this.domElement.addEventListener('pointerdown', this._onPointerDown)
this.domElement.addEventListener('pointerup', this._onPointerUp)

}

dispose() {
this.selectedObject = null
this.hoverObject = null

this.domElement.removeEventListener('pointermove', this._onPointerMove)
this.domElement.removeEventListener('pointerleave', this._onPointerLeave)
this.domElement.removeEventListener('pointerout', this._onPointerLeave)
this.domElement.removeEventListener('pointercancel', this._onPointerCancel)
this.domElement.removeEventListener('pointerenter', this._onPointerEnter)
this.domElement.removeEventListener('pointerdown', this._onPointerDown)
this.domElement.removeEventListener('pointerup', this._onPointerUp)
}

get camera() {
return this._camera
}

set camera(value) {
this._camera = value
}

get selectedObject(): IObject3D | null {
return this._selected.length > 0 ? this._selected[0] : null
}

set selectedObject(object) {
if (!this._selected.length && !object || this._selected.length === 1 && this._selected[0] === object) return
this._selected = object ? Array.isArray(object) ? [...object] : [object] : []
this.dispatchEvent({type: 'selectedObjectChanged', object: this.selectedObject})
}

get hoverObject() {
return this._hovering.length > 0 ? this._hovering[0] : null
}

set hoverObject(object: IObject3D | IObject3D[] | null) {
if (!this._hovering.length && !object || this._hovering.length === 1 && this._hovering[0] === object) return
this._hovering = object ? Array.isArray(object) ? [...object] : [object] : []
this.dispatchEvent({type: 'hoverObjectChanged', object: this.hoverObject})
}

get time() {
this._time = now()
return this._time
}

get isMouseDown() {
return this.mouseDownDeltaTime < 0
}

get mouseDownDeltaTime() {
return this._mouseUpTime - this._mouseDownTime
}

private _onPointerMove = (event: PointerEvent) => {

if (event.isPrimary === false) return
this.updateMouseFromEvent(event)

if (this.hoverEnabled)
this.hoverObject = this.checkIntersection()?.intersects[0].object ?? null

}

private _onPointerLeave = (event: PointerEvent) => {
if (event.isPrimary === false) return
this.domElement.style.cursor = this.cursorStyles.default

// this.updateMouseFromEvent(event);

if (this.hoverEnabled || this.hoverObject)
this.hoverObject = null

}

private _onPointerEnter = (_: PointerEvent) => {
// todo dispatch event?
}
private _onPointerCancel = (_: PointerEvent) => {
// todo dispatch event?
}

updateMouseFromEvent(event: PointerEvent) {
const rect = this.domElement.getBoundingClientRect()
this.mouse.x = (event.clientX - rect.x) / rect.width * 2 - 1
this.mouse.y = -((event.clientY - rect.y) / rect.height) * 2 + 1
}

private _onPointerDown = (event: PointerEvent) => {
if (event.isPrimary === false) return
this.domElement.style.cursor = this.cursorStyles.down

this._mouseDownTime = this.time

return undefined
}

private _onPointerUp = (event: PointerEvent) => {
if (event.isPrimary === false) return
this.domElement.style.cursor = this.cursorStyles.default

this._mouseUpTime = this.time
const delta = this.mouseDownDeltaTime
if (delta < 200) {
// click
this._onPointerClick(event)
}

return undefined
}

private _onPointerClick = (event: PointerEvent) => {
if (event.isPrimary === false) return
this.updateMouseFromEvent(event)

const intersects = this.checkIntersection()
if (intersects)
this.dispatchEvent({type: 'hitObject', time: this._mouseUpTime, intersects})
this.selectedObject = intersects?.selectedObject || null
}

checkIntersection() {
const camera = this._camera

if (!camera) return null

this.raycaster.setFromCamera(this.mouse, camera)

let intersects = this.raycaster.intersectObject<IObject3D>(this._root, true)

const uniqueIds: number[] = []

const uniqueIntersects = intersects.filter(element => {
const isDuplicate = uniqueIds.includes(element.object.id)

if (!isDuplicate) {
uniqueIds.push(element.object.id)
return true
}

return false
})

intersects = uniqueIntersects

let selectedObject:IObject3D | null = null
let intersect: Intersection<IObject3D> | undefined

const intersects2 = []
for (const intersect1 of intersects) {
selectedObject = intersect1.object
intersect = intersect1
while (selectedObject != null && (!selectedObject.visible || !this.selectionCondition(selectedObject))) {
selectedObject = selectedObject.parent
}
if (selectedObject != null) intersects2.push(intersect1)
}
intersects = intersects2

if (intersects.length > 0) {
selectedObject = intersects[0].object
intersect = intersects[0]

if (this._firstHit && selectedObject.id !== this._firstHit.id) {
selectedObject = intersect.object
} else {
for (let i = 0; i < intersects.length; i++) {
if (this.selectedObject && this.selectedObject.id === intersects[i].object.id) {
const n = i + 1 // Use ( i + 1 ) % intersects.length for looping through objects
if (n < intersects.length) {
intersect = intersects[n]
selectedObject = intersect.object
} else {
return null
}
}
}
}
this._firstHit = intersects[0].object
}

if (selectedObject && intersect) {

if (selectedObject) // sorted by distance
return {selectedObject, intersect, intersects, mouse: this.mouse.toArray()}

return null

} else {
return null
}

}

isHovering() {
return this.hoverObject != null // if something is highlighted.
}

isSelected() {
return this.selectedObject != null // if something is selected.
}

}

+ 110
- 0
src/three/utils/SelectionWidget.ts Näytä tiedosto

@@ -0,0 +1,110 @@
import {Group, Sphere, Vector2} from 'three'
import {LineMaterial} from 'three/examples/jsm/lines/LineMaterial.js'
import {AnyOptions} from 'ts-browser-helpers'
import {Box3B} from '../math/Box3B'
import {IObject3D, IWidget} from '../../core'
import {LineSegments2} from 'three/examples/jsm/lines/LineSegments2.js'
import {LineSegmentsGeometry} from 'three/examples/jsm/lines/LineSegmentsGeometry.js'

export class SelectionWidget extends Group implements IWidget {
isWidget = true as const

private _object: IObject3D | null = null
boundingScaleMultiplier = 1.
setDirty?: (options?: AnyOptions) => void

protected _updater() {
const selected: IObject3D | null | undefined = this._object
if (selected) {
const bbox = new Box3B().expandByObject(selected, false)
bbox.getCenter(this.position)
const scale = bbox.getBoundingSphere(new Sphere()).radius
this.scale.setScalar(scale * this.boundingScaleMultiplier)
this.setVisible(true)

} else {
this.setVisible(false)
}

}

constructor() {
super()

this.position.set(0, 0, 0)

this.visible = false
this.renderOrder = 100 // Don't draw too early, thus obscuring other transparent objects

this.userData.bboxVisible = false

this._updater = this._updater.bind(this)

}

setVisible(v: boolean) {
if (v !== this.visible) {
this.visible = v
this.setDirty?.({sceneUpdate: false})
}
}

attach(object: IObject3D): this {
this.detach()
if (!object) return this
this._object = object
this._object.addEventListener('objectUpdate', this._updater)
this._updater()
return this
}

detach(): this {
if (!this._object) return this
this._object?.removeEventListener('objectUpdate', this._updater)
this._object = null
this._updater()
return this
}

get object(): IObject3D | null {
return this._object
}

dispose() {
this.detach()
}

}

export class BoxSelectionWidget extends SelectionWidget {
constructor() {
super()
const matLine = new LineMaterial({
color: '#ff2222' as any, transparent: true, opacity: 0.9,
linewidth: 5, // in pixels
resolution: new Vector2(1024, 1024), // to be set by renderer, eventually
dashed: false,
toneMapped: false,
})

const ls = new LineSegmentsGeometry()
ls.setPositions([1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1].map(v=>v - 0.5))

const wireframe = new LineSegments2(ls, matLine)
wireframe.computeLineDistances()
wireframe.scale.set(1, 1, 1)
wireframe.visible = true
this.add(wireframe)
}

protected _updater() {
super._updater()
const selected = this.object
if (selected) {
const bbox = new Box3B().expandByObject(selected, false)
// const scale = bbox.getBoundingSphere(new Sphere()).radius
bbox.getSize(this.scale).multiplyScalar(this.boundingScaleMultiplier).clampScalar(0.1, 100)
this.setVisible(true)
}
}
}

+ 2
- 0
src/three/utils/index.ts Näytä tiedosto

@@ -6,5 +6,7 @@ export {getEncodingComponents, getTexelEncoding, getTexelDecoding, getTexelDecod
export {generateUUID, toIndexedGeometry, isInScene} from './misc'
export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDataUrl, texImageToCanvas} from './texture'
export {threeConstMappings} from './const-mappings'
export {ObjectPicker} from './ObjectPicker'
export {SelectionWidget, BoxSelectionWidget} from './SelectionWidget'

// export {} from './constants'

+ 1
- 1
src/viewer/version.ts Näytä tiedosto

@@ -1 +1 @@
export const VERSION = '0.0.14'
export const VERSION = '0.0.13'

Loading…
Peruuta
Tallenna