Added updated version of the viewer Catalogue extension. Updated dependancies

This commit is contained in:
AlexandruPopovici
2024-11-07 23:02:35 +02:00
parent 5be0a290bd
commit 5ee99a01e6
4 changed files with 383 additions and 0 deletions
+57
View File
@@ -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",
+2
View File
@@ -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",
+97
View File
@@ -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
}
}
+227
View File
@@ -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)
}
}