Added updated version of the viewer Catalogue extension. Updated dependancies
This commit is contained in:
Generated
+57
@@ -9,11 +9,13 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@speckle/viewer": "^2.22.1",
|
||||
"potpack": "2.0.0",
|
||||
"vue": "^3.5.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.17.6",
|
||||
"@types/three": "^0.169.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/eslint-config-prettier": "^10.1.0",
|
||||
"@vue/eslint-config-typescript": "^14.1.3",
|
||||
@@ -1724,6 +1726,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
@@ -1779,6 +1787,32 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz",
|
||||
"integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.169.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.169.0.tgz",
|
||||
"integrity": "sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": "*",
|
||||
"@webgpu/types": "*",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~0.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.20.tgz",
|
||||
"integrity": "sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz",
|
||||
@@ -2291,6 +2325,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.51",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.51.tgz",
|
||||
"integrity": "sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
@@ -3497,6 +3537,12 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -4365,6 +4411,12 @@
|
||||
"integrity": "sha512-WKghTBzqAvTt9rG5TWS78Dmk2kCCL9VkkX8Zi9kKfJ4iqYpvcGGpeYtkhPHa9NZAPLivZiZsdO/LBG3ENayDmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
|
||||
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -5190,6 +5242,11 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
|
||||
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw=="
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@speckle/viewer": "^2.22.1",
|
||||
"potpack": "2.0.0",
|
||||
"vue": "^3.5.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.17.6",
|
||||
"@types/three": "^0.169.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/eslint-config-prettier": "^10.1.0",
|
||||
"@vue/eslint-config-typescript": "^14.1.3",
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { BatchObject } from '@speckle/viewer'
|
||||
import { Vector3 } from 'three'
|
||||
|
||||
export interface Animation {
|
||||
target: BatchObject
|
||||
end: Vector3
|
||||
current: Vector3
|
||||
time: number
|
||||
}
|
||||
|
||||
const ZERO = 1e-8
|
||||
const ONE = 1 - 1e-8
|
||||
const vec3: Vector3 = new Vector3()
|
||||
|
||||
const easeOutQuart = (x: number) => {
|
||||
return 1 - Math.pow(1 - x, 4)
|
||||
}
|
||||
|
||||
export class AnimationGroup {
|
||||
/** We'll store our animations here */
|
||||
public animations: Animation[] = []
|
||||
/** Animation params */
|
||||
public animTimeScale: number = 0.25
|
||||
private reverse = false
|
||||
private _isAnimating = false
|
||||
|
||||
public onComplete: (() => void) | null = null
|
||||
|
||||
public get isAnimating(): boolean {
|
||||
return this._isAnimating
|
||||
}
|
||||
|
||||
public update(deltaTime: number): number {
|
||||
if (!this.animations.length || !this._isAnimating) return 0
|
||||
|
||||
let animCount = 0
|
||||
for (let k = 0; k < this.animations.length; k++) {
|
||||
/** Animation finished, no need to update it */
|
||||
if (this.animations[k].time === 1 || this.animations[k].time === 0) {
|
||||
continue
|
||||
}
|
||||
/** Compute the next animation time value */
|
||||
const t =
|
||||
this.animations[k].time +
|
||||
(this.reverse
|
||||
? -(deltaTime * this.animTimeScale)
|
||||
: deltaTime * this.animTimeScale)
|
||||
|
||||
/** Clamp it to 1 */
|
||||
this.animations[k].time = Math.min(Math.max(t, 0), 1)
|
||||
let easedT = easeOutQuart(this.animations[k].time)
|
||||
if (this.animations[k].time === 1) easedT = 1
|
||||
if (this.animations[k].time === 0) easedT = 0
|
||||
|
||||
/** Compute current position value based on animation time */
|
||||
vec3.set(0, 0, 0)
|
||||
const value = vec3.lerp(this.animations[k].end, easedT)
|
||||
/** Apply the translation */
|
||||
this.animations[k].target.transformTRS(
|
||||
value,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
)
|
||||
animCount++
|
||||
}
|
||||
|
||||
if (this._isAnimating && !animCount) {
|
||||
this._isAnimating = false
|
||||
if (this.onComplete) this.onComplete()
|
||||
}
|
||||
|
||||
return animCount
|
||||
}
|
||||
|
||||
public play() {
|
||||
for (let k = 0; k < this.animations.length; k++) {
|
||||
this.animations[k].time = ZERO
|
||||
}
|
||||
this.reverse = false
|
||||
this._isAnimating = true
|
||||
}
|
||||
|
||||
public playReverse() {
|
||||
for (let k = 0; k < this.animations.length; k++) {
|
||||
this.animations[k].time = ONE
|
||||
}
|
||||
this.reverse = true
|
||||
this._isAnimating = true
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.animations = []
|
||||
this.reverse = false
|
||||
this._isAnimating = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
BatchObject,
|
||||
NodeRenderView,
|
||||
ObjectLayers,
|
||||
SpeckleText,
|
||||
SpeckleTextMaterial,
|
||||
Extension,
|
||||
} from '@speckle/viewer'
|
||||
import potpack from 'potpack'
|
||||
import { Color, Matrix4, Vector3, Box3, DoubleSide, Group, Mesh } from 'three'
|
||||
import { AnimationGroup } from './AnimationGroup'
|
||||
|
||||
interface Box {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
interface ObjectBox extends Box {
|
||||
object: BatchObject
|
||||
}
|
||||
|
||||
interface CategoryBox extends Box {
|
||||
category: string
|
||||
boxes: Array<ObjectBox>
|
||||
}
|
||||
|
||||
export class Catalogue extends Extension {
|
||||
private animationGroup: AnimationGroup = new AnimationGroup()
|
||||
private textGroup: Group = new Group()
|
||||
|
||||
/** We're tying in to the viewer core's frame event */
|
||||
public onEarlyUpdate(deltaTime: number) {
|
||||
const animCount = this.animationGroup.update(deltaTime)
|
||||
|
||||
/** If any animations updated, request a render */
|
||||
if (animCount) {
|
||||
this.viewer.requestRender()
|
||||
}
|
||||
}
|
||||
|
||||
public animate(reverse: boolean = false) {
|
||||
if (reverse) this.animationGroup.playReverse()
|
||||
else this.animationGroup.play()
|
||||
/** After the next release we'll do this*/
|
||||
// this.viewer.getRenderer().resetPipeline(true)
|
||||
this.animationGroup.onComplete = () => {
|
||||
this.viewer.getRenderer().resetPipeline()
|
||||
}
|
||||
}
|
||||
|
||||
/** Example's main function */
|
||||
public async categorize(
|
||||
input: Array<{ ids: Array<string>; value: string }>,
|
||||
annotations = false,
|
||||
) {
|
||||
if (this.animationGroup.animations.length) return
|
||||
|
||||
const padding = 0.5
|
||||
const categoryPadding = 10
|
||||
const origin = new Vector3(0, 0, 0)
|
||||
|
||||
const objectBoxes: { [id: string]: ObjectBox } = {}
|
||||
const categories: { [categoryName: string]: CategoryBox } = {}
|
||||
|
||||
for (const cat of input) {
|
||||
const boxes: ObjectBox[] = []
|
||||
for (let k = 0; k < cat.ids.length; k++) {
|
||||
const nodes = this.viewer.getWorldTree().findId(cat.ids[k])
|
||||
if (!nodes) continue
|
||||
|
||||
/** Just get the first node */
|
||||
const node = nodes[0]
|
||||
|
||||
const rvs = this.viewer
|
||||
.getWorldTree()
|
||||
.getRenderTree()
|
||||
.getRenderViewsForNode(node)
|
||||
|
||||
const objects: BatchObject[] = rvs
|
||||
.map((rv: NodeRenderView) => {
|
||||
return this.viewer.getRenderer().getObject(rv)
|
||||
})
|
||||
.filter((value: BatchObject | null) => {
|
||||
return value && !objectBoxes[value.renderView.renderData.id]
|
||||
}) as BatchObject[]
|
||||
|
||||
objects.forEach((object: BatchObject) => {
|
||||
if (!object) return
|
||||
const aabbSize = object?.aabb.getSize(new Vector3())
|
||||
const box = {
|
||||
object,
|
||||
w: aabbSize.x + padding,
|
||||
h: aabbSize.y + padding,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
objectBoxes[object.renderView.renderData.id] = box
|
||||
boxes.push(box)
|
||||
})
|
||||
|
||||
if (!objects.length) continue
|
||||
|
||||
const { w, h } = potpack(boxes)
|
||||
categories[cat.value] = {
|
||||
category: cat.value,
|
||||
boxes,
|
||||
w: w + categoryPadding,
|
||||
h: h + categoryPadding,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
}
|
||||
potpack(Object.values(categories))
|
||||
console.log(categories)
|
||||
}
|
||||
|
||||
this.makeAnimations(categories, origin)
|
||||
if (annotations) await this.makeAnnotations(categories, origin)
|
||||
}
|
||||
|
||||
private makeAnimations(
|
||||
categories: { [categoryName: string]: CategoryBox },
|
||||
origin: Vector3,
|
||||
) {
|
||||
for (const k in categories) {
|
||||
for (let i = 0; i < categories[k].boxes.length; i++) {
|
||||
const objectBox = categories[k].boxes[i]
|
||||
const box3 = new Box3(
|
||||
new Vector3(objectBox.x + origin.x, objectBox.y + origin.y, 0),
|
||||
new Vector3(
|
||||
objectBox.x + origin.x + objectBox.w,
|
||||
objectBox.y + origin.y + objectBox.h,
|
||||
0,
|
||||
),
|
||||
).applyMatrix4(
|
||||
new Matrix4().makeTranslation(categories[k].x, categories[k].y, 0),
|
||||
)
|
||||
|
||||
const bObj = objectBox.object
|
||||
const boxCenter = box3.getCenter(new Vector3())
|
||||
const aabbCenter = bObj.aabb.getCenter(new Vector3())
|
||||
const aabbSize = bObj.aabb.getSize(new Vector3())
|
||||
const finalPos = new Vector3()
|
||||
.copy(boxCenter)
|
||||
.sub(aabbCenter.sub(new Vector3(0, 0, aabbSize.z * 0.5)))
|
||||
|
||||
this.animationGroup.animations.push({
|
||||
target: bObj,
|
||||
end: finalPos,
|
||||
current: new Vector3(),
|
||||
time: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async makeAnnotations(
|
||||
categories: { [categoryName: string]: CategoryBox },
|
||||
origin: Vector3,
|
||||
) {
|
||||
for (const categoryBox in categories) {
|
||||
/** Create a speckle text object */
|
||||
const text = new SpeckleText('test-text', ObjectLayers.OVERLAY)
|
||||
|
||||
/** Simple text material */
|
||||
const material = new SpeckleTextMaterial(
|
||||
{
|
||||
color: 0x1a1a1a,
|
||||
opacity: 1,
|
||||
side: DoubleSide,
|
||||
},
|
||||
['USE_RTE', 'BILLBOARD_FIXED'],
|
||||
)
|
||||
material.toneMapped = false
|
||||
material.color.convertSRGBToLinear()
|
||||
material.opacity = 1
|
||||
material.transparent = false
|
||||
material.depthTest = false
|
||||
material.billboardPixelHeight = 20
|
||||
material.userData.billboardPos.value.copy(text.position)
|
||||
;(text.textMesh as unknown as Mesh).material =
|
||||
material.getDerivedMaterial()
|
||||
|
||||
if (text.backgroundMesh) text.backgroundMesh.renderOrder = 3
|
||||
;(text.textMesh as unknown as Mesh).renderOrder = 4
|
||||
|
||||
/** Set the layers to PROPS, so that AO and interactions will ignore them */
|
||||
text.layers.set(ObjectLayers.OVERLAY)
|
||||
;(text.textMesh as unknown as Mesh).layers.set(ObjectLayers.OVERLAY)
|
||||
/** Update the text with the cateogry name, size and anchor */
|
||||
await text
|
||||
.update({
|
||||
textValue: categories[categoryBox].category,
|
||||
height: 1,
|
||||
anchorX: '50%',
|
||||
anchorY: '43%',
|
||||
})
|
||||
.then(() => {
|
||||
text.style = {
|
||||
textColor: new Color(0x1a1a1a),
|
||||
backgroundColor: new Color(0xffffff),
|
||||
billboard: true,
|
||||
backgroundPixelHeight: 20,
|
||||
}
|
||||
/** Move the text to the bottom center of the category box */
|
||||
text.setTransform(
|
||||
new Vector3(
|
||||
origin.x + categories[categoryBox].x,
|
||||
origin.y + categories[categoryBox].y,
|
||||
0,
|
||||
),
|
||||
)
|
||||
})
|
||||
/** Add the text to the scene */
|
||||
this.textGroup.add(text)
|
||||
}
|
||||
this.viewer.getRenderer().scene.add(this.textGroup)
|
||||
}
|
||||
|
||||
public wipe() {
|
||||
this.animationGroup.clear()
|
||||
this.textGroup.clear()
|
||||
this.viewer.getRenderer().scene.remove(this.textGroup)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user