diff --git a/.circleci/config.yml b/.circleci/config.yml index ec9fa3bade..b392f5c301 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -348,7 +348,7 @@ jobs: type: string docker: - image: speckle/pre-commit-runner:latest - resource_class: medium + resource_class: large working_directory: *work-dir steps: - checkout @@ -416,6 +416,8 @@ jobs: S3_CREATE_BUCKET: 'true' REDIS_URL: 'redis://127.0.0.1:6379' S3_REGION: '' # optional, defaults to 'us-east-1' + FF_AUTOMATE_MODULE_ENABLED: 'true' + AUTOMATE_ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json' steps: - checkout - restore_cache: diff --git a/.husky/pre-commit b/.husky/pre-commit index b9acddca1c..9c3e8a14da 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -6,7 +6,10 @@ if [ -n "$CI" ] then echo "running eslint" yarn lint + echo "...eslint done" + echo "running prettier" yarn prettier:check + echo "...prettier done" else # shellcheck disable=SC1090 . "$(dirname "$0")/_/husky.sh" diff --git a/docker-compose-ingress.yml b/docker-compose-ingress.yml new file mode 100644 index 0000000000..38f99bce02 --- /dev/null +++ b/docker-compose-ingress.yml @@ -0,0 +1,9 @@ +services: + nginx: + restart: always + image: nginx:1-alpine + ports: + - 8080:8080 + volumes: + - ./utils/docker-compose-ingress/nginx/default.conf:/etc/nginx/conf.d/default.conf + network_mode: host diff --git a/package.json b/package.json index 3b1e6004cd..8af86b2c36 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build:public": "yarn workspaces foreach -ptv --no-private run build", "build:tailwind-deps": "yarn workspaces foreach -iv -j unlimited --include '{@speckle/shared,@speckle/tailwind-theme,@speckle/ui-components}' run build", "ensure:tailwind-deps": "node ./utils/ensure-tailwind-deps.mjs", - "lint": "eslint . --ext .js,.ts,.vue --max-warnings=0", + "lint": "node --max-old-space-size=4096 ./node_modules/eslint/bin/eslint.js . --ext .js,.ts,.vue --max-warnings=0", "helm:readme:generate": "./utils/helm/update-schema-json.sh", "prettier:check": "prettier --check .", "prettier:fix": "prettier --write .", @@ -45,6 +45,7 @@ "@types/eslint": "^8.4.1", "@types/lockfile": "^1.0.2", "commitizen": "^4.2.5", + "cross-env": "^7.0.3", "cz-conventional-changelog": "^3.3.0", "eslint": "^8.11.0", "eslint-config-prettier": "^8.5.0", diff --git a/packages/frontend-2/assets/images/banners/speckleverse.svg b/packages/frontend-2/assets/images/banners/speckleverse.svg new file mode 100644 index 0000000000..593dee2380 --- /dev/null +++ b/packages/frontend-2/assets/images/banners/speckleverse.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/frontend-2/components/auth/LoginPanel.vue b/packages/frontend-2/components/auth/LoginPanel.vue index ece8aa6c10..9045f187da 100644 --- a/packages/frontend-2/components/auth/LoginPanel.vue +++ b/packages/frontend-2/components/auth/LoginPanel.vue @@ -1,6 +1,7 @@ diff --git a/packages/frontend-2/components/automate/function/Card.vue b/packages/frontend-2/components/automate/function/Card.vue index 5f59d2b855..5808b4b9de 100644 --- a/packages/frontend-2/components/automate/function/Card.vue +++ b/packages/frontend-2/components/automate/function/Card.vue @@ -20,7 +20,12 @@ {{ fn.name }} -
by {{ fn.creator?.name || 'Deleted User' }}
+
+ by + + {{ fn.repo.owner }} + +
@@ -52,10 +57,14 @@
- + +
@@ -75,8 +84,10 @@ graphql(` isFeatured description logo - creator { + repo { id + url + owner name } } @@ -93,10 +104,11 @@ const props = defineProps<{ noButtons?: boolean externalMoreInfo?: boolean selected?: boolean + isOutdated?: boolean }>() const NuxtLink = resolveComponent('NuxtLink') -const hasLabel = computed(() => props.fn.isFeatured) +const hasLabel = computed(() => props.fn.isFeatured || props.isOutdated) const { html: plaintextDescription } = useMarkdown( computed(() => props.fn.description || ''), { plaintext: true } diff --git a/packages/frontend-2/components/automate/function/CreateDialog.vue b/packages/frontend-2/components/automate/function/CreateDialog.vue index 0e3e928d2b..c632b971c0 100644 --- a/packages/frontend-2/components/automate/function/CreateDialog.vue +++ b/packages/frontend-2/components/automate/function/CreateDialog.vue @@ -172,6 +172,7 @@ const buttons = computed((): LayoutDialogButton[] => { case FunctionCreateSteps.Authorize: return [ { + id: 'authorizeClose', text: 'Close', props: { color: 'secondary', @@ -180,6 +181,7 @@ const buttons = computed((): LayoutDialogButton[] => { onClick: () => (open.value = false) }, { + id: 'authorizeAuthorize', text: 'Authorize', props: { fullWidth: true, @@ -192,6 +194,7 @@ const buttons = computed((): LayoutDialogButton[] => { case FunctionCreateSteps.Template: return [ { + id: 'templateNext', text: 'Next', props: { iconRight: ChevronRightIcon, @@ -203,6 +206,7 @@ const buttons = computed((): LayoutDialogButton[] => { case FunctionCreateSteps.Details: return [ { + id: 'detailsPrevious', text: 'Previous', props: { color: 'secondary', @@ -212,6 +216,7 @@ const buttons = computed((): LayoutDialogButton[] => { onClick: () => step.value-- }, { + id: 'detailsCreate', text: 'Create', submit: true, disabled: mutationLoading.value @@ -220,6 +225,7 @@ const buttons = computed((): LayoutDialogButton[] => { case FunctionCreateSteps.Done: return [ { + id: 'doneClose', text: 'Close', props: { color: 'secondary', @@ -228,6 +234,7 @@ const buttons = computed((): LayoutDialogButton[] => { onClick: () => (open.value = false) }, { + id: 'doneGoToFunction', text: 'Go to Function', props: { iconRight: ArrowRightIcon, diff --git a/packages/frontend-2/components/automate/function/create-dialog/DoneStep.vue b/packages/frontend-2/components/automate/function/create-dialog/DoneStep.vue index bc8016fed5..4505f14b57 100644 --- a/packages/frontend-2/components/automate/function/create-dialog/DoneStep.vue +++ b/packages/frontend-2/components/automate/function/create-dialog/DoneStep.vue @@ -37,14 +37,18 @@ import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/outline' import { graphql } from '~/lib/common/generated/gql' import { buildGithubRepoHttpCloneUrl, - buildGithubRepoSshUrl, - parseGithubRepoUrl + buildGithubRepoSshUrl } from '~/lib/common/helpers/github' graphql(` fragment AutomateFunctionCreateDialogDoneStep_AutomateFunction on AutomateFunction { id - repoUrl + repo { + id + url + owner + name + } ...AutomationsFunctionsCard_AutomateFunction } `) @@ -53,21 +57,18 @@ const props = defineProps<{ createdFunction: AutomateFunctionCreateDialogDoneStep_AutomateFunctionFragment }>() -const repoDetails = computed(() => parseGithubRepoUrl(props.createdFunction.repoUrl)) - const repoCodespaceLink = computed(() => { - if (!repoDetails.value) return undefined - - return `https://codespaces.new/${repoDetails.value.owner}/${repoDetails.value.name}` + const { owner, name } = props.createdFunction.repo + return `https://codespaces.new/${owner}/${name}` }) -const repoLink = computed(() => props.createdFunction.repoUrl) +const repoLink = computed(() => props.createdFunction.repo.url) const cloneInstructions = computed(() => { - if (!repoDetails.value) return '' + const repo = props.createdFunction.repo - const htmlUrl = buildGithubRepoHttpCloneUrl(repoDetails.value) - const sshUrl = buildGithubRepoSshUrl(repoDetails.value) + const htmlUrl = buildGithubRepoHttpCloneUrl(repo) + const sshUrl = buildGithubRepoSshUrl(repo) return `# Clone the repository using SSH (recommended) git clone ${sshUrl} diff --git a/packages/frontend-2/components/automate/function/page/Header.vue b/packages/frontend-2/components/automate/function/page/Header.vue index 1479a8f4c3..a0c62077e1 100644 --- a/packages/frontend-2/components/automate/function/page/Header.vue +++ b/packages/frontend-2/components/automate/function/page/Header.vue @@ -48,8 +48,11 @@ graphql(` id name logo - creator { + repo { id + url + owner + name } } `) diff --git a/packages/frontend-2/components/automate/function/page/Info.vue b/packages/frontend-2/components/automate/function/page/Info.vue index c16530c757..8f5d8cff1e 100644 --- a/packages/frontend-2/components/automate/function/page/Info.vue +++ b/packages/frontend-2/components/automate/function/page/Info.vue @@ -28,7 +28,7 @@ Github account icon @@ -114,7 +114,12 @@ import type { AutomateFunctionPageInfo_AutomateFunctionFragment } from '~/lib/co graphql(` fragment AutomateFunctionPageInfo_AutomateFunction on AutomateFunction { id - repoUrl + repo { + id + url + owner + name + } automationCount description releases(limit: 1) { @@ -137,7 +142,7 @@ const props = defineProps<{ fn: AutomateFunctionPageInfo_AutomateFunctionFragment }>() -const repoUrl = computed(() => props.fn.repoUrl) +const repoUrl = computed(() => props.fn.repo.url) const latestRelease = computed(() => props.fn.releases.items.length ? props.fn.releases.items[0] : undefined ) diff --git a/packages/frontend-2/components/automate/runs/StatusBadge.vue b/packages/frontend-2/components/automate/runs/StatusBadge.vue index f0af9c6801..3e4b2084d0 100644 --- a/packages/frontend-2/components/automate/runs/StatusBadge.vue +++ b/packages/frontend-2/components/automate/runs/StatusBadge.vue @@ -1,8 +1,5 @@ diff --git a/packages/frontend-2/components/automate/runs/trigger-status/Dialog.vue b/packages/frontend-2/components/automate/runs/trigger-status/Dialog.vue index eb60e9ce2d..9710193a48 100644 --- a/packages/frontend-2/components/automate/runs/trigger-status/Dialog.vue +++ b/packages/frontend-2/components/automate/runs/trigger-status/Dialog.vue @@ -21,16 +21,13 @@
-
- -
+
- - diff --git a/packages/frontend-2/components/automate/runs/trigger-status/dialog/FunctionRun.vue b/packages/frontend-2/components/automate/runs/trigger-status/dialog/FunctionRun.vue index b3a3d92796..16bf943e64 100644 --- a/packages/frontend-2/components/automate/runs/trigger-status/dialog/FunctionRun.vue +++ b/packages/frontend-2/components/automate/runs/trigger-status/dialog/FunctionRun.vue @@ -111,6 +111,8 @@ graphql(` logo name } + createdAt + updatedAt } `) diff --git a/packages/frontend-2/components/automate/runs/trigger-status/dialog/RunsRows.vue b/packages/frontend-2/components/automate/runs/trigger-status/dialog/RunsRows.vue new file mode 100644 index 0000000000..fa47e78188 --- /dev/null +++ b/packages/frontend-2/components/automate/runs/trigger-status/dialog/RunsRows.vue @@ -0,0 +1,40 @@ + + diff --git a/packages/frontend-2/components/automate/viewer/Panel.vue b/packages/frontend-2/components/automate/viewer/Panel.vue index 772ca956b4..33b59b9528 100644 --- a/packages/frontend-2/components/automate/viewer/Panel.vue +++ b/packages/frontend-2/components/automate/viewer/Panel.vue @@ -17,20 +17,19 @@
- +
diff --git a/packages/frontend-2/components/automate/viewer/panel/FunctionRunRow.vue b/packages/frontend-2/components/automate/viewer/panel/FunctionRunRow.vue index a4ad52f1dd..ab0f85fe57 100644 --- a/packages/frontend-2/components/automate/viewer/panel/FunctionRunRow.vue +++ b/packages/frontend-2/components/automate/viewer/panel/FunctionRunRow.vue @@ -135,6 +135,8 @@ graphql(` logo name } + createdAt + updatedAt } `) diff --git a/packages/frontend-2/components/common/TitleDescription.vue b/packages/frontend-2/components/common/TitleDescription.vue new file mode 100644 index 0000000000..0060773b65 --- /dev/null +++ b/packages/frontend-2/components/common/TitleDescription.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/frontend-2/components/error/page/Renderer.vue b/packages/frontend-2/components/error/page/Renderer.vue index 428b80075a..04dec35db2 100644 --- a/packages/frontend-2/components/error/page/Renderer.vue +++ b/packages/frontend-2/components/error/page/Renderer.vue @@ -3,7 +3,7 @@

Error {{ finalError.statusCode || 500 }}

-

+

{{ finalError.message }}

diff --git a/packages/frontend-2/components/form/json/Form.vue b/packages/frontend-2/components/form/json/Form.vue index 003cb5b47e..1a8d020ef4 100644 --- a/packages/frontend-2/components/form/json/Form.vue +++ b/packages/frontend-2/components/form/json/Form.vue @@ -15,6 +15,7 @@ import type { JsonSchema, UISchemaElement } from '@jsonforms/core' import { JsonForms, type JsonFormsChangeEvent } from '@jsonforms/vue' import type { Nullable, Optional } from '@speckle/shared' +import { useMounted } from '@vueuse/core' import { omit } from 'lodash-es' import { useForm } from 'vee-validate' import { renderers } from '~/lib/form/jsonRenderers' @@ -23,14 +24,19 @@ type DataType = Record const emit = defineEmits<(e: 'change', val: JsonFormsChangeEvent) => void>() -const props = defineProps<{ - schema: JsonSchema - uiSchema?: UISchemaElement - readonly?: boolean -}>() +const props = withDefaults( + defineProps<{ + schema: JsonSchema + uiSchema?: UISchemaElement + readonly?: boolean + validateOnMount?: boolean + }>(), + { validateOnMount: true } +) const { validate } = useForm() +const isMounted = useMounted() const internalRef = ref>(null) const data = defineModel>('data') @@ -52,7 +58,10 @@ const autoGeneratedUiSchema = computed(() => { const finalUiSchema = computed(() => props.uiSchema || autoGeneratedUiSchema.value) const onChange = async (e: JsonFormsChangeEvent) => { - // console.log(JSON.parse(JSON.stringify(e))) + if (!isMounted.value && !props.validateOnMount) { + return + } + data.value = e.data as DataType await validate({ mode: 'force' }) emit('change', e) @@ -67,7 +76,15 @@ const getFormState = (): Optional => } as JsonFormsChangeEvent) : undefined -defineExpose({ getFormState }) +const triggerChange = async () => { + const state = getFormState() + if (state) { + await onChange(state) + } + return state +} + +defineExpose({ getFormState, triggerChange }) diff --git a/packages/ui-components/src/components/layout/DialogSection.vue b/packages/ui-components/src/components/layout/DialogSection.vue index f78795bb51..59e635fc99 100644 --- a/packages/ui-components/src/components/layout/DialogSection.vue +++ b/packages/ui-components/src/components/layout/DialogSection.vue @@ -55,7 +55,7 @@
-
Title: {{ activeItem?.title }}
-
ID: {{ activeItem?.id }}
- -
` - }), - args: { - items, - title: 'Settings', - activeItem: items[0] - } -} - -export const Vertical: StoryObj = { - ...Default, - args: { - ...Default.args, - vertical: true - } -} - -export const WithActiveItemModel: StoryObj = { - ...Default, - args: { - ...Default.args, - activeItem: items[2] - } -} - -export const WithActiveItemModelBlocked: StoryObj = { - ...WithActiveItemModel, - render: (args, ctx) => ({ - components: { LayoutPageTabs }, - setup() { - const { model } = useStorybookVmodel({ - args, - prop: 'activeItem', - ctx, - blockChanges: true - }) - return { args, model } - }, - template: ` -
- -
Title: {{ activeItem?.title }}
-
ID: {{ activeItem?.id }}
-
-
` - }) -} diff --git a/packages/ui-components/src/components/layout/PageTabs.vue b/packages/ui-components/src/components/layout/PageTabs.vue deleted file mode 100644 index ab68847773..0000000000 --- a/packages/ui-components/src/components/layout/PageTabs.vue +++ /dev/null @@ -1,138 +0,0 @@ - - diff --git a/packages/ui-components/src/components/layout/Table.vue b/packages/ui-components/src/components/layout/Table.vue index 5ba17f94e1..3c1eac1c82 100644 --- a/packages/ui-components/src/components/layout/Table.vue +++ b/packages/ui-components/src/components/layout/Table.vue @@ -4,6 +4,7 @@ class="w-full text-sm overflow-x-auto overflow-y-visible simple-scrollbar border border-outline-3 rounded-lg" >
diff --git a/packages/ui-components/src/components/layout/Tabs.stories.ts b/packages/ui-components/src/components/layout/Tabs.stories.ts deleted file mode 100644 index 8112981916..0000000000 --- a/packages/ui-components/src/components/layout/Tabs.stories.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3' -import LayoutTabs from '~~/src/components/layout/Tabs.vue' -import type { LayoutTabItem } from '~~/src/helpers/layout/components' - -export default { - component: LayoutTabs, - parameters: { - docs: { - description: { - component: 'Standard tabs component' - } - } - } -} as Meta - -const defaultItems: LayoutTabItem[] = [ - { title: 'First tab', id: 'first' }, - { title: 'Second tab', id: 'second' } -] - -export const Default: StoryObj = { - render: (args) => ({ - components: { LayoutTabs }, - setup() { - return { args } - }, - template: ` -
- -
Title: {{ activeItem.title }}
-
ID: {{ activeItem.id }}
-
-
` - }), - args: { - items: defaultItems - } -} diff --git a/packages/ui-components/src/components/layout/Tabs.vue b/packages/ui-components/src/components/layout/Tabs.vue deleted file mode 100644 index eb33975a31..0000000000 --- a/packages/ui-components/src/components/layout/Tabs.vue +++ /dev/null @@ -1,37 +0,0 @@ - - diff --git a/packages/ui-components/src/components/layout/tabs/Horizontal.stories.ts b/packages/ui-components/src/components/layout/tabs/Horizontal.stories.ts new file mode 100644 index 0000000000..eb7c035217 --- /dev/null +++ b/packages/ui-components/src/components/layout/tabs/Horizontal.stories.ts @@ -0,0 +1,67 @@ +import type { Meta, StoryObj } from '@storybook/vue3' +import LayoutTabsHorizontal from '~~/src/components/layout/tabs/Horizontal.vue' +import type { LayoutPageTabItem } from '~~/src/helpers/layout/components' +import { useStorybookVmodel } from '~~/src/composables/testing' + +const items: LayoutPageTabItem[] = [ + { title: 'Models', id: 'models', count: 300 }, + { title: 'Discussions', id: 'discussions' }, + { title: 'Automations', id: 'automations', tag: 'New' }, + { title: 'Settings', id: 'settings' }, + { + title: 'Disabled Item', + id: 'disabled', + disabled: true, + disabledMessage: 'Example disabled message' + } +] + +export default { + component: LayoutTabsHorizontal, + parameters: { + docs: { + description: { + component: + 'This component displays a set of horizontal tabs, allowing user interaction and selection.' + } + } + }, + argTypes: { + items: { + description: 'Array of items to display in the tabs' + }, + title: { + description: 'Title of the tabs, displayed above the tabs if provided' + }, + activeItem: { + description: 'The active item model. Not required.' + }, + 'update:activeItem': { + description: 'Event emitted when the active item changes', + type: 'function', + action: 'v-model:activeItem' + } + } +} as Meta + +export const Default: StoryObj = { + render: (args, ctx) => ({ + components: { LayoutTabsHorizontal }, + setup() { + const { model } = useStorybookVmodel({ args, prop: 'activeItem', ctx }) + return { args, model } + }, + template: ` +
+ +
Title: {{ activeItem?.title }}
+
ID: {{ activeItem?.id }}
+
+
` + }), + args: { + items, + title: 'Tab Example', + activeItem: items[2] + } +} diff --git a/packages/ui-components/src/components/layout/tabs/Horizontal.vue b/packages/ui-components/src/components/layout/tabs/Horizontal.vue new file mode 100644 index 0000000000..1fc12fc530 --- /dev/null +++ b/packages/ui-components/src/components/layout/tabs/Horizontal.vue @@ -0,0 +1,241 @@ + + + + diff --git a/packages/ui-components/src/components/layout/tabs/Vertical.stories.ts b/packages/ui-components/src/components/layout/tabs/Vertical.stories.ts new file mode 100644 index 0000000000..d7099ff20c --- /dev/null +++ b/packages/ui-components/src/components/layout/tabs/Vertical.stories.ts @@ -0,0 +1,67 @@ +import type { Meta, StoryObj } from '@storybook/vue3' +import LayoutTabsVertical from '~~/src/components/layout/tabs/Vertical.vue' +import type { LayoutPageTabItem } from '~~/src/helpers/layout/components' +import { useStorybookVmodel } from '~~/src/composables/testing' + +const items: LayoutPageTabItem[] = [ + { title: 'Models', id: 'models', count: 300 }, + { title: 'Discussions', id: 'discussions' }, + { title: 'Automations', id: 'automations', tag: 'New' }, + { title: 'Settings', id: 'settings' }, + { + title: 'Disabled Item', + id: 'disabled', + disabled: true, + disabledMessage: 'Example disabled message' + } +] + +export default { + component: LayoutTabsVertical, + parameters: { + docs: { + description: { + component: + 'This component displays a set of vertical tabs, allowing user interaction and selection.' + } + } + }, + argTypes: { + items: { + description: 'Array of items to display in the tabs' + }, + title: { + description: 'Title of the tabs, displayed above the tabs if provided' + }, + activeItem: { + description: 'The active item model. Not required.' + }, + 'update:activeItem': { + description: 'Event emitted when the active item changes', + type: 'function', + action: 'v-model:activeItem' + } + } +} as Meta + +export const Default: StoryObj = { + render: (args, ctx) => ({ + components: { LayoutTabsVertical }, + setup() { + const { model } = useStorybookVmodel({ args, prop: 'activeItem', ctx }) + return { args, model } + }, + template: ` +
+ +
Title: {{ activeItem?.title }}
+
ID: {{ activeItem?.id }}
+
+
` + }), + args: { + items, + title: 'Tab Example', + activeItem: items[2] + } +} diff --git a/packages/ui-components/src/components/layout/tabs/Vertical.vue b/packages/ui-components/src/components/layout/tabs/Vertical.vue new file mode 100644 index 0000000000..46e1f130ab --- /dev/null +++ b/packages/ui-components/src/components/layout/tabs/Vertical.vue @@ -0,0 +1,107 @@ + + + diff --git a/packages/ui-components/src/composables/form/textInput.ts b/packages/ui-components/src/composables/form/textInput.ts index 30ba602533..e607a9fb89 100644 --- a/packages/ui-components/src/composables/form/textInput.ts +++ b/packages/ui-components/src/composables/form/textInput.ts @@ -46,7 +46,10 @@ export function useTextInputCore(params: { }) const labelClasses = computed(() => { - const classParts = ['block label text-foreground-2 mb-2'] + const classParts = [ + 'flex label mb-1.5', + unref(props.color) === 'foundation' ? 'text-foreground' : 'text-foreground-2' + ] if (!unref(props.showLabel)) { classParts.push('sr-only') } @@ -58,7 +61,7 @@ export function useTextInputCore(params: { const classParts: string[] = [ 'focus:outline-none disabled:cursor-not-allowed disabled:bg-foundation-disabled', 'disabled:text-disabled-muted placeholder:text-foreground-2', - 'rounded' + 'rounded-md' ] return classParts.join(' ') @@ -80,7 +83,9 @@ export function useTextInputCore(params: { const color = unref(props.color) if (color === 'foundation') { - classParts.push('bg-foundation shadow-sm hover:shadow') + classParts.push( + 'bg-foundation !border border-outline-3 focus:border-outline-1 focus:!outline-0 focus:!ring-0' + ) } else if (color === 'transparent') { classParts.push('bg-transparent') } else { @@ -108,7 +113,7 @@ export function useTextInputCore(params: { hasHelpTip.value ? `${unref(props.name)}-${internalHelpTipId.value}` : undefined ) const helpTipClasses = computed((): string => { - const classParts = ['mt-2 text-xs sm:text-sm'] + const classParts = ['mt-2 text-xs'] classParts.push(error.value ? 'text-danger' : 'text-foreground-2') return classParts.join(' ') }) diff --git a/packages/ui-components/src/helpers/layout/components.ts b/packages/ui-components/src/helpers/layout/components.ts index 60192ad57f..53cb347188 100644 --- a/packages/ui-components/src/helpers/layout/components.ts +++ b/packages/ui-components/src/helpers/layout/components.ts @@ -2,6 +2,7 @@ import type { ConcreteComponent } from 'vue' import type { FormButton } from '~~/src/lib' type FormButtonProps = InstanceType['$props'] +import type { PropAnyComponent } from '~~/src/helpers/common/components' export enum GridListToggleValue { Grid = 'grid', @@ -16,9 +17,11 @@ export type LayoutTabItem = { export type LayoutPageTabItem = { title: string id: I - icon?: ConcreteComponent count?: number tag?: string + icon?: PropAnyComponent + disabled?: boolean + disabledMessage?: string } export type LayoutMenuItem = { @@ -33,7 +36,13 @@ export type LayoutMenuItem = { export type LayoutDialogButton = { text: string props?: Record & FormButtonProps - onClick?: () => void + onClick?: (e: MouseEvent) => void disabled?: boolean submit?: boolean + /** + * This should uniquely identify the button within the form. Even if you have different sets + * of buttons rendered on different steps of a wizard, all of them should have unique IDs to + * ensure proper form functionality. + */ + id?: string } diff --git a/packages/ui-components/src/lib.ts b/packages/ui-components/src/lib.ts index 4c8bb336cc..a83c6e23f9 100644 --- a/packages/ui-components/src/lib.ts +++ b/packages/ui-components/src/lib.ts @@ -21,6 +21,7 @@ import CommonVimeoEmbed from '~~/src/components/common/VimeoEmbed.vue' import FormCardButton from '~~/src/components/form/CardButton.vue' import FormCheckbox from '~~/src/components/form/Checkbox.vue' import FormRadio from '~~/src/components/form/Radio.vue' +import FormRadioGroup from '~~/src/components/form/RadioGroup.vue' import FormTextArea from '~~/src/components/form/TextArea.vue' import FormTextInput from '~~/src/components/form/TextInput.vue' import * as ValidationHelpers from '~~/src/helpers/common/validation' @@ -57,8 +58,8 @@ import { } from '~~/src/composables/common/window' import LayoutMenu from '~~/src/components/layout/Menu.vue' import type { LayoutMenuItem, LayoutTabItem } from '~~/src/helpers/layout/components' -import LayoutTabs from '~~/src/components/layout/Tabs.vue' -import LayoutPageTabs from '~~/src/components/layout/PageTabs.vue' +import LayoutTabsHoriztonal from '~~/src/components/layout/tabs/Horizontal.vue' +import LayoutTabsVertical from '~~/src/components/layout/tabs/Vertical.vue' import LayoutTable from '~~/src/components/layout/Table.vue' import InfiniteLoading from '~~/src/components/InfiniteLoading.vue' import type { InfiniteLoaderState } from '~~/src/helpers/global/components' @@ -114,6 +115,7 @@ export { FormCardButton, FormCheckbox, FormRadio, + FormRadioGroup, FormTextArea, FormTextInput, FormSwitch, @@ -141,8 +143,8 @@ export { useOnBeforeWindowUnload, useResponsiveHorizontalDirectionCalculation, LayoutMenu, - LayoutTabs, - LayoutPageTabs, + LayoutTabsHoriztonal, + LayoutTabsVertical, LayoutTable, InfiniteLoading, LayoutPanel, diff --git a/packages/viewer-sandbox/.eslintrc.js b/packages/viewer-sandbox/.eslintrc.js index 5a57524a34..6d9efc2ad8 100644 --- a/packages/viewer-sandbox/.eslintrc.js +++ b/packages/viewer-sandbox/.eslintrc.js @@ -11,6 +11,9 @@ const config = { parserOptions: { sourceType: 'module' }, + rules: { + '@typescript-eslint/no-non-null-assertion': 'error' + }, overrides: [ { files: '*.ts', diff --git a/packages/viewer-sandbox/src/Extensions/BoxSelection.ts b/packages/viewer-sandbox/src/Extensions/BoxSelection.ts index d390115247..ee369b7624 100644 --- a/packages/viewer-sandbox/src/Extensions/BoxSelection.ts +++ b/packages/viewer-sandbox/src/Extensions/BoxSelection.ts @@ -4,13 +4,7 @@ import { ObjectLayers } from '@speckle/viewer' import { NodeRenderView } from '@speckle/viewer' import { SelectionExtension } from '@speckle/viewer' import { BatchObject } from '@speckle/viewer' -import { - Extension, - IViewer, - GeometryType, - MeshBatch, - CameraController -} from '@speckle/viewer' +import { Extension, IViewer, GeometryType, CameraController } from '@speckle/viewer' import { Matrix4, ShaderMaterial, @@ -113,17 +107,18 @@ export class BoxSelection extends Extension { /** Gets the object ids that fall withing the provided selection box */ private getSelectionIds(selectionBox: Box3) { + /** Get the renderer */ + const renderer = this.viewer.getRenderer() /** Get the mesh batches */ - const batches = this.viewer - .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] - + const batches = renderer.batcher.getBatches(undefined, GeometryType.MESH) /** Compute the clip matrix */ const clipMatrix = new Matrix4() - clipMatrix.multiplyMatrices( - this.viewer.getRenderer().renderingCamera.projectionMatrix, - this.viewer.getRenderer().renderingCamera.matrixWorldInverse - ) + if (renderer.renderingCamera) { + clipMatrix.multiplyMatrices( + renderer.renderingCamera.projectionMatrix, + renderer.renderingCamera.matrixWorldInverse + ) + } /** We're using three-mesh-bvh library for out BVH * Go over each batch and test it against the TAS only. diff --git a/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts b/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts index 7ed3378368..0815712eb8 100644 --- a/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts +++ b/packages/viewer-sandbox/src/Extensions/CameraPlanes.ts @@ -3,7 +3,6 @@ import { Extension, GeometryType, IViewer, - MeshBatch, Vector3 } from '@speckle/viewer' import { PerspectiveCamera } from 'three' @@ -20,9 +19,7 @@ export class CameraPlanes extends Extension { public constructor(viewer: IViewer) { super(viewer) - this.camerController = viewer.getExtension( - CameraController as new () => CameraController - ) + this.camerController = viewer.getExtension(CameraController) as CameraController } public onEarlyUpdate(): void { @@ -30,7 +27,10 @@ export class CameraPlanes extends Extension { } public computePerspectiveCameraPlanes() { - const camera = this.viewer.getRenderer().renderingCamera as PerspectiveCamera + const renderer = this.viewer.getRenderer() + if (!renderer.renderingCamera) return + + const camera = renderer.renderingCamera as PerspectiveCamera const minDist = this.getClosestGeometryDistance(camera) if (minDist === Number.POSITIVE_INFINITY) return @@ -42,7 +42,7 @@ export class CameraPlanes extends Extension { 1 + Math.pow(Math.tan(((fov / 180) * Math.PI) / 2), 2) * (Math.pow(aspect, 2) + 1) ) - this.viewer.getRenderer().renderingCamera.near = nearPlane + renderer.renderingCamera.near = nearPlane console.log(minDist, nearPlane) } @@ -55,11 +55,13 @@ export class CameraPlanes extends Extension { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + .batcher.getBatches(undefined, GeometryType.MESH) let minDist = Number.POSITIVE_INFINITY const minPoint = new Vector3() for (let b = 0; b < batches.length; b++) { const result = batches[b].mesh.TAS.closestPointToPoint(cameraPosition) + if (!result) continue + const planarity = cameraDir.dot( new Vector3().subVectors(result.point, cameraPosition).normalize() ) diff --git a/packages/viewer-sandbox/src/Extensions/ExtendedSelection.ts b/packages/viewer-sandbox/src/Extensions/ExtendedSelection.ts index b49c61b318..6777802a32 100644 --- a/packages/viewer-sandbox/src/Extensions/ExtendedSelection.ts +++ b/packages/viewer-sandbox/src/Extensions/ExtendedSelection.ts @@ -34,10 +34,14 @@ export class ExtendedSelection extends SelectionExtension { } private initGizmo() { + const rendeder = this.viewer.getRenderer() + if (!rendeder.renderingCamera) + throw new Error('Cannot use ExtendedSelection without a rendering camera') + /** Create a new TransformControls gizmo */ this.transformControls = new TransformControls( - this.viewer.getRenderer().renderingCamera, - this.viewer.getRenderer().renderer.domElement + rendeder.renderingCamera, + rendeder.renderer.domElement ) /** The gizmo creates an entire hierarchy of children internally, * and three.js objects do not inherit parent layer values, so diff --git a/packages/viewer-sandbox/src/Sandbox.ts b/packages/viewer-sandbox/src/Sandbox.ts index 1d941b1ca4..fd06d03c94 100644 --- a/packages/viewer-sandbox/src/Sandbox.ts +++ b/packages/viewer-sandbox/src/Sandbox.ts @@ -2,7 +2,7 @@ import { Box3, SectionTool, SpeckleStandardMaterial, TreeNode } from '@speckle/viewer' import { CanonicalView, - DebugViewer, + Viewer, PropertyInfo, SelectionEvent, SunLightConfiguration, @@ -14,7 +14,8 @@ import { DiffExtension, SpeckleLoader, ObjLoader, - UrlHelper + UrlHelper, + LoaderEvent } from '@speckle/viewer' import { FolderApi, Pane } from 'tweakpane' import { DiffResult } from '@speckle/viewer' @@ -25,7 +26,7 @@ import { FilteringExtension } from '@speckle/viewer' import { MeasurementsExtension } from '@speckle/viewer' import { CameraController } from '@speckle/viewer' import { UpdateFlags } from '@speckle/viewer' -import { Viewer, AssetType, Assets } from '@speckle/viewer' +import { AssetType, Assets } from '@speckle/viewer' import Neutral from '../assets/hdri/Neutral.png' import Mild from '../assets/hdri/Mild.png' import Mild2 from '../assets/hdri/Mild2.png' @@ -133,7 +134,7 @@ export default class Sandbox { public constructor( container: HTMLElement, - viewer: DebugViewer, + viewer: Viewer, selectionList: SelectionEvent[] ) { this.viewer = viewer @@ -164,20 +165,17 @@ export default class Sandbox { this.batchesParams.totalBvhSize = this.getBVHSize() this.refresh() }) - viewer.on(ViewerEvent.UnloadComplete, async (url: string) => { - url + viewer.on(ViewerEvent.UnloadComplete, async () => { this.removeViewControls() this.addViewControls() this.properties = await this.viewer.getObjectProperties() }) - viewer.on(ViewerEvent.UnloadAllComplete, async (url: string) => { + viewer.on(ViewerEvent.UnloadAllComplete, async () => { this.removeViewControls() this.addViewControls() this.properties = await this.viewer.getObjectProperties() - // viewer.World.resetWorld() - url }) - viewer.on(ViewerEvent.ObjectClicked, (selectionEvent: SelectionEvent) => { + viewer.on(ViewerEvent.ObjectClicked, (selectionEvent) => { if (selectionEvent && selectionEvent.hits) { const firstHitNode = selectionEvent.hits[0].node if (firstHitNode) { @@ -218,13 +216,14 @@ export default class Sandbox { }) const position = { value: { x: 0, y: 0, z: 0 } } folder.addInput(position, 'value', { label: 'Position' }).on('change', () => { - const rvs = this.viewer - .getWorldTree() - .getRenderTree(url) - .getRenderViewsForNodeId(url) - for (let k = 0; k < rvs.length; k++) { - const object = this.viewer.getRenderer().getObject(rvs[k]) - object.transformTRS(position.value, undefined, undefined, undefined) + const tree = this.viewer.getWorldTree() + const rvs = tree.getRenderTree(url)?.getRenderViewsForNodeId(url) + if (rvs) { + for (let k = 0; k < rvs.length; k++) { + const object = this.viewer.getRenderer().getObject(rvs[k]) + if (object) + object.transformTRS(position.value, undefined, undefined, undefined) + } } this.viewer.requestRender(UpdateFlags.RENDER | UpdateFlags.SHADOWS) this.viewer.getRenderer().updateShadowCatcher() @@ -276,10 +275,7 @@ export default class Sandbox { title: `Object: ${node.model.id}` }) - const rvs = this.viewer - .getWorldTree() - .getRenderTree() - .getRenderViewsForNode(node, node) + const rvs = this.viewer.getWorldTree().getRenderTree().getRenderViewsForNode(node) const objects: BatchObject[] = [] for (let k = 0; k < rvs.length; k++) { const batchObject = this.viewer.getRenderer().getObject(rvs[k]) @@ -344,7 +340,7 @@ export default class Sandbox { .on('change', () => { const unionBox: Box3 = new Box3() objects.forEach((obj: BatchObject) => { - unionBox.union(obj.renderView.aabb) + unionBox.union(obj.renderView.aabb || new Box3()) }) const origin = unionBox.getCenter(new Vector3()) objects.forEach((obj: BatchObject) => { @@ -577,7 +573,7 @@ export default class Sandbox { .on('change', () => { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + .batcher.getBatches(undefined, GeometryType.MESH) batches.forEach((batch: MeshBatch) => { const materials = batch.materials as SpeckleStandardMaterial[] materials.forEach((material: SpeckleStandardMaterial) => { @@ -595,7 +591,7 @@ export default class Sandbox { .on('change', () => { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + .batcher.getBatches(undefined, GeometryType.MESH) batches.forEach((batch: MeshBatch) => { const materials = batch.materials as SpeckleStandardMaterial[] materials.forEach((material: SpeckleStandardMaterial) => { @@ -617,7 +613,7 @@ export default class Sandbox { .on('change', () => { const batches = this.viewer .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + .batcher.getBatches(undefined, GeometryType.MESH) batches.forEach((batch: MeshBatch) => { const materials = batch.materials as SpeckleStandardMaterial[] materials.forEach((material: SpeckleStandardMaterial) => { @@ -947,6 +943,13 @@ export default class Sandbox { this.viewer.setLightConfiguration(this.lightParams) }) + const updateShadowcatcher = () => { + const shadowCatcher = this.viewer.getRenderer().shadowcatcher + if (shadowCatcher) { + shadowCatcher.configuration = this.shadowCatcherParams + this.viewer.getRenderer().updateShadowCatcher() + } + } shadowcatcherFolder .addInput(this.shadowCatcherParams, 'textureSize', { label: 'Texture Size', @@ -954,10 +957,8 @@ export default class Sandbox { max: 1024, step: 1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'weights', { @@ -967,10 +968,8 @@ export default class Sandbox { z: { min: -100, max: 100 }, w: { min: -100, max: 100 } }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'blurRadius', { @@ -979,10 +978,8 @@ export default class Sandbox { max: 128, step: 1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'stdDeviation', { @@ -991,10 +988,8 @@ export default class Sandbox { max: 128, step: 1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'sigmoidRange', { @@ -1003,10 +998,8 @@ export default class Sandbox { max: 10, step: 0.1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) shadowcatcherFolder .addInput(this.shadowCatcherParams, 'sigmoidStrength', { @@ -1015,10 +1008,8 @@ export default class Sandbox { max: 10, step: 0.1 }) - .on('change', (value) => { - value - this.viewer.getRenderer().shadowcatcher.configuration = this.shadowCatcherParams - this.viewer.getRenderer().updateShadowCatcher() + .on('change', () => { + updateShadowcatcher() }) } @@ -1301,9 +1292,19 @@ export default class Sandbox { url, authToken, true, - undefined, - 1 + undefined ) + /** Too spammy */ + // loader.on(LoaderEvent.LoadProgress, (arg: { progress: number; id: string }) => { + // console.warn(arg) + // }) + loader.on(LoaderEvent.LoadCancelled, (resource: string) => { + console.warn(`Resource ${resource} loading was canceled`) + }) + loader.on(LoaderEvent.LoadWarning, (arg: { message: string }) => { + console.error(`Loader warning: ${arg.message}`) + }) + await this.viewer.loadObject(loader, true) } localStorage.setItem('last-load-url', url) diff --git a/packages/viewer-sandbox/src/main-multi.ts b/packages/viewer-sandbox/src/main-multi.ts index 75324f2d24..dd9cc3b433 100644 --- a/packages/viewer-sandbox/src/main-multi.ts +++ b/packages/viewer-sandbox/src/main-multi.ts @@ -2,13 +2,7 @@ import { DefaultViewerParams, SelectionEvent, ViewerEvent, - DebugViewer, - Viewer -} from '@speckle/viewer' - -import './style.css' -import Sandbox from './Sandbox' -import { + Viewer, CameraController, SelectionExtension, SectionTool, @@ -16,15 +10,8 @@ import { MeasurementsExtension } from '@speckle/viewer' -// const container0 = document.querySelector('#renderer0') -// if (!container0) { -// throw new Error("Couldn't find #app container!") -// } - -// const container1 = document.querySelector('#renderer1') -// if (!container1) { -// throw new Error("Couldn't find #app container!") -// } +import './style.css' +import Sandbox from './Sandbox' const createViewer = async (containerName: string, stream: string) => { const container = document.querySelector(containerName) @@ -44,7 +31,7 @@ const createViewer = async (containerName: string, stream: string) => { params.verbose = true const multiSelectList: SelectionEvent[] = [] - const viewer: Viewer = new DebugViewer(container, params) + const viewer: Viewer = new Viewer(container, params) await viewer.init() const cameraController = viewer.createExtension(CameraController) @@ -58,21 +45,12 @@ const createViewer = async (containerName: string, stream: string) => { sectionOutlines // use it measurements // use it - const sandbox = new Sandbox(controlsContainer, viewer as DebugViewer, multiSelectList) + const sandbox = new Sandbox(controlsContainer, viewer, multiSelectList) window.addEventListener('load', () => { viewer.resize() }) - viewer.on( - ViewerEvent.LoadProgress, - (a: { progress: number; id: string; url: string }) => { - if (a.progress >= 1) { - viewer.resize() - } - } - ) - viewer.on(ViewerEvent.LoadComplete, () => { Object.assign(sandbox.sceneParams.worldSize, viewer.World.worldSize) Object.assign(sandbox.sceneParams.worldOrigin, viewer.World.worldOrigin) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 78f7d76a98..7289d139ef 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -4,7 +4,6 @@ import { DefaultViewerParams, SelectionEvent, ViewerEvent, - DebugViewer, Viewer } from '@speckle/viewer' @@ -40,7 +39,7 @@ const createViewer = async (containerName: string, stream: string) => { params.verbose = true const multiSelectList: SelectionEvent[] = [] - const viewer: Viewer = new DebugViewer(container, params) + const viewer: Viewer = new Viewer(container, params) await viewer.init() const cameraController = viewer.createExtension(CameraController) @@ -64,27 +63,15 @@ const createViewer = async (containerName: string, stream: string) => { // rotateCamera // use it // boxSelect // use it - const sandbox = new Sandbox(controlsContainer, viewer as DebugViewer, multiSelectList) + const sandbox = new Sandbox(controlsContainer, viewer, multiSelectList) window.addEventListener('load', () => { viewer.resize() }) - viewer.on( - ViewerEvent.ObjectClicked, - (event: { hits: { node: { model: { id: string } } }[] }) => { - if (event) console.log(event.hits[0].node.model.id) - } - ) - - viewer.on( - ViewerEvent.LoadProgress, - (a: { progress: number; id: string; url: string }) => { - if (a.progress >= 1) { - viewer.resize() - } - } - ) + viewer.on(ViewerEvent.ObjectClicked, (event: SelectionEvent | null) => { + if (event) console.log(event.hits[0].node.model.id) + }) viewer.on(ViewerEvent.LoadComplete, async () => { console.warn(viewer.getRenderer().renderingStats) @@ -121,6 +108,7 @@ const getStream = () => { // 'https://speckle.xyz/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D' // Revit sample house (good for bim-like stuff with many display meshes) 'https://speckle.xyz/streams/da9e320dad/commits/5388ef24b8' + // 'https://latest.speckle.dev/streams/c1faab5c62/commits/ab1a1ab2b6' // 'https://speckle.xyz/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.dev/streams/58b5648c4d/commits/60371ecb2d' // 'Super' heavy revit shit @@ -347,6 +335,7 @@ const getStream = () => { // 'https://latest.speckle.dev/streams/ee5346d3e1/commits/576310a6d5' // 'https://latest.speckle.dev/streams/ee5346d3e1/commits/489d42ca8c' // 'https://latest.speckle.dev/streams/97750296c2/objects/11a7752e40b4ef0620affc55ce9fdf5a' + // 'https://speckle.xyz/streams/0ed2cdc8eb/commits/350c4e1a4d' // 'https://latest.speckle.dev/streams/92b620fb17/objects/7118603b197c00944f53be650ce721ec' diff --git a/packages/viewer/.eslintrc.cjs b/packages/viewer/.eslintrc.cjs index dbeae6a4a4..8eff2ff389 100644 --- a/packages/viewer/.eslintrc.cjs +++ b/packages/viewer/.eslintrc.cjs @@ -18,7 +18,8 @@ const config = { } }, rules: { - 'no-console': ['warn', { allow: ['warn', 'error'] }] + 'no-console': ['warn', { allow: ['warn', 'error'] }], + '@typescript-eslint/no-non-null-assertion': 'error' }, ignorePatterns: ['dist2', 'example/speckleviewer.web.js'], overrides: [ diff --git a/packages/viewer/package.json b/packages/viewer/package.json index b037623822..33aa79a40b 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -56,18 +56,15 @@ "@speckle/shared": "workspace:^", "@types/flat": "^5.0.2", "camera-controls": "^1.33.1", - "flat": "^5.0.2", "hold-event": "^0.1.0", "js-logger": "1.6.1", "lodash-es": "^4.17.21", - "rainbowvis.js": "^1.0.1", "string-to-color": "^2.2.2", "three": "^0.140.0", "three-mesh-bvh": "0.5.17", "tree-model": "1.0.7", "troika-three-text": "0.47.2", - "type-fest": "^4.15.0", - "underscore": "1.13.6" + "type-fest": "^4.15.0" }, "devDependencies": { "@babel/core": "^7.18.2", @@ -76,6 +73,7 @@ "@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-image": "^3.0.2", "@types/babel__core": "^7.20.1", + "@types/lodash-es": "4.17.12", "@types/three": "^0.136.0", "@typescript-eslint/eslint-plugin": "^5.39.0", "@typescript-eslint/parser": "^5.39.0", diff --git a/packages/viewer/src/IViewer.ts b/packages/viewer/src/IViewer.ts index 52d7a0840d..e68768a9c8 100644 --- a/packages/viewer/src/IViewer.ts +++ b/packages/viewer/src/IViewer.ts @@ -1,16 +1,31 @@ import { Vector3 } from 'three' +import { type PropertyInfo } from './modules/filtering/PropertyManager' +import type { Query, QueryArgsResultMap } from './modules/queries/Query' +import { type TreeNode, WorldTree } from './modules/tree/WorldTree' +import { type Utils } from './modules/Utils' import defaultHdri from './assets/hdri/Mild-dwab.png' -import { PropertyInfo } from './modules/filtering/PropertyManager' -import { Query, QueryArgsResultMap, QueryResult } from './modules/queries/Query' -import { DataTree } from './modules/tree/DataTree' -import { TreeNode, WorldTree } from './modules/tree/WorldTree' -import { Utils } from './modules/Utils' import { World } from './modules/World' import SpeckleRenderer from './modules/SpeckleRenderer' import { Extension } from './modules/extensions/Extension' -import Input from './modules/input/Input' import { Loader } from './modules/loaders/Loader' import { type Constructor } from 'type-fest' +import type { Vector3Like } from './modules/batching/BatchObject' +import type { FilteringState } from './modules/extensions/FilteringExtension' + +export type SpeckleReference = { + referencedId: string +} + +export type SpeckleObject = { + [k: string]: unknown + speckle_type: string + id: string + elements?: SpeckleReference[] + children?: SpeckleObject[] | SpeckleReference[] + name?: string + referencedId?: string + units?: string +} export interface ViewerParams { showStats: boolean @@ -54,21 +69,29 @@ export const DefaultViewerParams: ViewerParams = { export enum ViewerEvent { ObjectClicked = 'object-clicked', ObjectDoubleClicked = 'object-doubleclicked', - DownloadComplete = 'download-complete', LoadComplete = 'load-complete', - LoadProgress = 'load-progress', UnloadComplete = 'unload-complete', - LoadCancelled = 'load-cancelled', UnloadAllComplete = 'unload-all-complete', Busy = 'busy', FilteringStateSet = 'filtering-state-set', LightConfigUpdated = 'light-config-updated' } +export interface ViewerEventPayload { + [ViewerEvent.ObjectClicked]: SelectionEvent | null + [ViewerEvent.ObjectDoubleClicked]: SelectionEvent | null + [ViewerEvent.LoadComplete]: string + [ViewerEvent.UnloadComplete]: string + [ViewerEvent.UnloadAllComplete]: void + [ViewerEvent.Busy]: boolean + [ViewerEvent.FilteringStateSet]: FilteringState + [ViewerEvent.LightConfigUpdated]: LightConfiguration +} + export type SpeckleView = { name: string id: string - view: Record + view: { origin: Vector3Like; target: Vector3Like } } export type SelectionEvent = { @@ -140,14 +163,16 @@ export enum StencilOutlineType { } export interface IViewer { - get input(): Input get Utils(): Utils get World(): World init(): Promise resize(): void - on(eventType: ViewerEvent, handler: (arg) => void) - requestRender(flags?: number): void + on( + eventType: T, + handler: (arg: ViewerEventPayload[T]) => void + ): void + requestRender(flags?: UpdateFlags): void setLightConfiguration(config: LightConfiguration): void @@ -166,16 +191,14 @@ export interface IViewer { ): Promise /** Data ops */ - getDataTree(): DataTree getWorldTree(): WorldTree - query(query: T): QueryArgsResultMap[T['operation']] - queryAsync(query: Query): Promise + query(query: T): QueryArgsResultMap[T['operation']] | null getRenderer(): SpeckleRenderer getContainer(): HTMLElement createExtension(type: Constructor): T getExtension(type: Constructor): T - + hasExtension(type: Constructor): boolean dispose(): void } diff --git a/packages/viewer/src/helpers/flatten.ts b/packages/viewer/src/helpers/flatten.ts index 819d1e674f..bf1e743b17 100644 --- a/packages/viewer/src/helpers/flatten.ts +++ b/packages/viewer/src/helpers/flatten.ts @@ -4,7 +4,7 @@ * @param obj object to flatten * @returns an object with all its props flattened into `prop.subprop.subsubprop`. */ -const flattenObject = function (obj) { +const flattenObject = function (obj: { [x: string]: unknown; id: unknown }) { const flatten = {} as Record for (const k in obj) { if ( @@ -19,7 +19,7 @@ const flattenObject = function (obj) { const v = obj[k] if (v === null || v === undefined || Array.isArray(v)) continue if (v.constructor === Object) { - const flattenProp = flattenObject(v) + const flattenProp = flattenObject(v as { [x: string]: unknown; id: unknown }) for (const pk in flattenProp) { flatten[k + '.' + pk] = flattenProp[pk] } diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index ca30e7228e..233f87cc72 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -3,75 +3,78 @@ import { AssetType, DefaultLightConfiguration, DefaultViewerParams, - IViewer, + type IViewer, ObjectLayers, - SelectionEvent, - SpeckleView, + type SelectionEvent, + type SpeckleObject, + type SpeckleReference, + type SpeckleView, UpdateFlags, ViewerEvent, - ViewerParams + type ViewerParams, + LightConfiguration, + ViewerEventPayload } from './IViewer' -import { +import type { PropertyInfo, StringPropertyInfo, NumericPropertyInfo } from './modules/filtering/PropertyManager' -import { SunLightConfiguration } from './IViewer' -import { DataTree, ObjectPredicate, SpeckleObject } from './modules/tree/DataTree' +import { type SunLightConfiguration } from './IViewer' import { World } from './modules/World' -import { DebugViewer } from './modules/DebugViewer' -import { NodeData, TreeNode, WorldTree } from './modules/tree/WorldTree' -import { +import { type NodeData, type TreeNode, WorldTree } from './modules/tree/WorldTree' +import type { PointQuery, QueryResult, IntersectionQuery, PointQueryResult, IntersectionQueryResult } from './modules/queries/Query' -import { Utils } from './modules/Utils' +import { type Utils } from './modules/Utils' import { BatchObject } from './modules/batching/BatchObject' import { Box3, Vector3 } from 'three' import { - MeasurementOptions, + type MeasurementOptions, MeasurementType, MeasurementsExtension } from './modules/extensions/measurements/MeasurementsExtension' import { Units } from './modules/converter/Units' import { SelectionExtension } from './modules/extensions/SelectionExtension' import { CameraController } from './modules/extensions/CameraController' -import { InlineView } from './modules/extensions/CameraController' -import { CanonicalView } from './modules/extensions/CameraController' -import { CameraEvent } from './modules/objects/SpeckleCamera' -import { SectionTool } from './modules/extensions/SectionTool' +import { type InlineView } from './modules/extensions/CameraController' +import { type CanonicalView } from './modules/extensions/CameraController' +import { CameraEvent, CameraEventPayload } from './modules/objects/SpeckleCamera' +import { SectionTool, SectionToolEventPayload } from './modules/extensions/SectionTool' import { SectionOutlines } from './modules/extensions/SectionOutlines' import { FilteringExtension, - FilteringState + type FilteringState } from './modules/extensions/FilteringExtension' import { Extension } from './modules/extensions/Extension' import { ExplodeExtension } from './modules/extensions/ExplodeExtension' import { DiffExtension, - DiffResult, + type DiffResult, VisualDiffMode } from './modules/extensions/DiffExtension' -import { Loader } from './modules/loaders/Loader' +import { Loader, LoaderEvent } from './modules/loaders/Loader' import { SpeckleLoader } from './modules/loaders/Speckle/SpeckleLoader' import { ObjLoader } from './modules/loaders/OBJ/ObjLoader' import { LegacyViewer } from './modules/LegacyViewer' import { SpeckleType } from './modules/loaders/GeometryConverter' -import Input, { InputEvent } from './modules/input/Input' +import Input, { InputEvent, InputEventPayload } from './modules/input/Input' import { GeometryType } from './modules/batching/Batch' import { MeshBatch } from './modules/batching/MeshBatch' import SpeckleStandardMaterial from './modules/materials/SpeckleStandardMaterial' import SpeckleTextMaterial from './modules/materials/SpeckleTextMaterial' import { SpeckleText } from './modules/objects/SpeckleText' import { NodeRenderView } from './modules/tree/NodeRenderView' +import { type ExtendedIntersection } from './modules/objects/SpeckleRaycaster' +import { SpeckleGeometryConverter } from './modules/loaders/Speckle/SpeckleGeometryConverter' import { Assets } from './modules/Assets' export { Viewer, - DebugViewer, LegacyViewer, DefaultViewerParams, ViewerEvent, @@ -97,6 +100,7 @@ export { Loader, SpeckleLoader, ObjLoader, + LoaderEvent, UpdateFlags, SpeckleType, Input, @@ -108,6 +112,7 @@ export { SpeckleTextMaterial, SpeckleText, NodeRenderView, + SpeckleGeometryConverter, Assets, AssetType } @@ -119,10 +124,10 @@ export type { PropertyInfo, StringPropertyInfo, NumericPropertyInfo, + LightConfiguration, SunLightConfiguration, - DataTree, - ObjectPredicate, SpeckleObject, + SpeckleReference, SpeckleView, CanonicalView, InlineView, @@ -136,7 +141,12 @@ export type { Utils, DiffResult, MeasurementOptions, - FilteringState + FilteringState, + ExtendedIntersection, + ViewerEventPayload, + InputEventPayload, + SectionToolEventPayload, + CameraEventPayload } export * as UrlHelper from './modules/UrlHelper' diff --git a/packages/viewer/src/modules/Assets.ts b/packages/viewer/src/modules/Assets.ts index ac4f483c3a..ff7b635746 100644 --- a/packages/viewer/src/modules/Assets.ts +++ b/packages/viewer/src/modules/Assets.ts @@ -4,20 +4,24 @@ import { TextureLoader, Color, DataTexture, + DataTextureLoader, Matrix4, Euler } from 'three' import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js' import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js' import { FontLoader, Font } from 'three/examples/jsm/loaders/FontLoader.js' -import { Asset, AssetType } from '../IViewer' +import { type Asset, AssetType } from '../IViewer' import Logger from 'js-logger' import { RotatablePMREMGenerator } from './objects/RotatablePMREMGenerator' export class Assets { private static _cache: { [name: string]: Texture | Font } = {} - private static getLoader(src: string, assetType: AssetType): TextureLoader { + private static getLoader( + src: string, + assetType: AssetType + ): TextureLoader | DataTextureLoader | null { if (assetType === undefined) assetType = src.split('.').pop() as AssetType if (!Object.values(AssetType).includes(assetType)) { Logger.warn(`Asset ${src} could not be loaded. Unknown type`) @@ -30,6 +34,8 @@ export class Assets { return new RGBELoader() case AssetType.TEXTURE_8BPP: return new TextureLoader() + default: + return null } } @@ -115,7 +121,7 @@ export class Assets { } public static getFont(asset: Asset | string): Promise { - let srcUrl: string = null + let srcUrl: string | null = null if ((asset).src) { srcUrl = (asset as Asset).src } else { @@ -128,7 +134,7 @@ export class Assets { return new Promise((resolve, reject) => { new FontLoader().load( - srcUrl, + srcUrl as string, (font: Font) => { resolve(font) }, @@ -148,6 +154,14 @@ export class Assets { canvas.height = texture.image.height const context = canvas.getContext('2d') + /** As you can see here https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext#return_value + * The only valid cases where `getContext` returns null are: + * - "contextType doesn't match a possible drawing context" Definetely not the case as we're providing '2d'! + * - "differs from the first contextType requested". It can't since **we're only requesting a context once**! + * - If it returns null outside of these two casese, you have bigger problems than us throwing an exception here + */ + if (!context) throw new Error('Fatal! 2d context could not be retrieved.') + context.drawImage(texture.image, 0, 0) const data = context.getImageData(0, 0, canvas.width, canvas.height) diff --git a/packages/viewer/src/modules/DebugViewer.ts b/packages/viewer/src/modules/DebugViewer.ts deleted file mode 100644 index 2d63377f9e..0000000000 --- a/packages/viewer/src/modules/DebugViewer.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Viewer } from './Viewer' - -export class DebugViewer extends Viewer { - requestRenderShadowmap() { - this.getRenderer().updateDirectLights() - this.requestRender() - } -} diff --git a/packages/viewer/src/modules/Intersections.ts b/packages/viewer/src/modules/Intersections.ts index 1d9d667536..43ddae12f9 100644 --- a/packages/viewer/src/modules/Intersections.ts +++ b/packages/viewer/src/modules/Intersections.ts @@ -1,7 +1,6 @@ import { Box3, Camera, - Intersection, Object3D, Plane, Ray, @@ -12,11 +11,15 @@ import { } from 'three' import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js' -import { SpeckleRaycaster } from './objects/SpeckleRaycaster' +import { + ExtendedIntersection, + ExtendedMeshIntersection, + SpeckleRaycaster +} from './objects/SpeckleRaycaster' import { ObjectLayers } from '../IViewer' export class Intersections { - private raycaster: SpeckleRaycaster + protected raycaster: SpeckleRaycaster private boxBuffer: Box3 = new Box3() private vec0Buffer: Vector4 = new Vector4() private vec1Buffer: Vector4 = new Vector4() @@ -26,12 +29,11 @@ export class Intersections { this.raycaster = new SpeckleRaycaster() this.raycaster.params.Line = { threshold: 0.01 } this.raycaster.params.Points = { threshold: 0.01 } - ;(this.raycaster.params as { Line2? }).Line2 = {} - ;(this.raycaster.params as { Line2? }).Line2.threshold = 1 + this.raycaster.params.Line2 = { threshold: 1 } this.raycaster.onObjectIntersectionTest = this.onObjectIntersection.bind(this) } - private onObjectIntersection(obj: Object3D) { + protected onObjectIntersection(obj: Object3D) { if (obj instanceof LineSegments2) { const box = this.boxBuffer.setFromObject(obj) const min = this.vec0Buffer.set(box.min.x, box.min.y, box.min.z, 1) @@ -62,17 +64,11 @@ export class Intersections { * original line width and how zoomed in the camer is on the line(batch) */ if (!worldSpace) { - if (ssDistance < 1) { - ;(this.raycaster.params as { Line2? }).Line2.threshold = lineWidth * 8 - } else { - ;(this.raycaster.params as { Line2? }).Line2.threshold = lineWidth * 5 - } + this.raycaster.params.Line2.threshold = + ssDistance < 1 ? lineWidth * 8 : lineWidth * 5 } else { - if (ssDistance < 1) { - ;(this.raycaster.params as { Line2? }).Line2.threshold = lineWidth * 2 - } else { - ;(this.raycaster.params as { Line2? }).Line2.threshold = lineWidth - } + this.raycaster.params.Line2.threshold = + ssDistance < 1 ? lineWidth * 2 : lineWidth } } } @@ -81,54 +77,111 @@ export class Intersections { scene: Scene, camera: Camera, point: Vector2, + castLayers: ObjectLayers.STREAM_CONTENT_MESH, + nearest?: boolean, + bounds?: Box3, + firstOnly?: boolean + ): Array | null + public intersect( + scene: Scene, + camera: Camera, + point: Vector2, + castLayers?: Array, + nearest?: boolean, + bounds?: Box3, + firstOnly?: boolean + ): Array | null + + public intersect( + scene: Scene, + camera: Camera, + point: Vector2, + castLayers: Array | ObjectLayers | undefined = undefined, nearest = true, - bounds: Box3 = null, - castLayers: Array = undefined, + bounds?: Box3, firstOnly = false - ): Array { + ): Array | Array | null { this.raycaster.setFromCamera(point, camera) this.raycaster.firstHitOnly = firstOnly - return this.intersectInternal(scene, nearest, bounds, castLayers) + const preserveMask = this.setRaycasterLayers(castLayers) + let result: Array | Array | null + if (castLayers === ObjectLayers.STREAM_CONTENT_MESH) { + result = this.intersectInternal(scene, nearest, bounds) + } else result = this.intersectInternal(scene, nearest, bounds) + this.raycaster.layers.mask = preserveMask + return result } public intersectRay( scene: Scene, camera: Camera, ray: Ray, + castLayers: ObjectLayers.STREAM_CONTENT_MESH, + nearest?: boolean, + bounds?: Box3, + firstOnly?: boolean + ): Array | null + public intersectRay( + scene: Scene, + camera: Camera, + ray: Ray, + castLayers?: Array, + nearest?: boolean, + bounds?: Box3, + firstOnly?: boolean + ): Array | null + + public intersectRay( + scene: Scene, + camera: Camera, + ray: Ray, + castLayers: Array | ObjectLayers | undefined = undefined, nearest = true, - bounds: Box3 = null, - castLayers: Array = undefined, + bounds?: Box3, firstOnly = false - ): Array { + ): Array | Array | null { this.raycaster.camera = camera this.raycaster.set(ray.origin, ray.direction) this.raycaster.firstHitOnly = firstOnly - return this.intersectInternal(scene, nearest, bounds, castLayers) + const preserveMask = this.setRaycasterLayers(castLayers) + let result: Array | Array | null + if (castLayers === ObjectLayers.STREAM_CONTENT_MESH) { + result = this.intersectInternal(scene, nearest, bounds) + } else result = this.intersectInternal(scene, nearest, bounds) + this.raycaster.layers.mask = preserveMask + return result } - private intersectInternal( - scene: Scene, - nearest: boolean, - bounds: Box3, - castLayers: Array - ) { + private setRaycasterLayers( + castLayers: Array | ObjectLayers | undefined + ): number { const preserveMask = this.raycaster.layers.mask - if (castLayers !== undefined) { this.raycaster.layers.disableAll() - castLayers.forEach((layer) => { - this.raycaster.layers.enable(layer) - }) + if (Array.isArray(castLayers)) + castLayers.forEach((layer) => { + this.raycaster.layers.enable(layer) + }) + else { + this.raycaster.layers.enable(castLayers) + } } + return preserveMask + } + + private intersectInternal( + scene: Scene, + nearest?: boolean, + bounds?: Box3 + ): T[] | null { + let results: T[] | null = [] const target = scene.getObjectByName('ContentGroup') - let results = [] if (target) { // const start = performance.now() results = this.raycaster.intersectObjects(target.children) // Logger.warn('Interesct time -> ', performance.now() - start) } - this.raycaster.layers.mask = preserveMask if (results.length === 0) return null if (nearest) diff --git a/packages/viewer/src/modules/LegacyViewer.ts b/packages/viewer/src/modules/LegacyViewer.ts index ad1aa862b2..5aec7f7c60 100644 --- a/packages/viewer/src/modules/LegacyViewer.ts +++ b/packages/viewer/src/modules/LegacyViewer.ts @@ -1,39 +1,48 @@ -import { MathUtils } from 'three' -import { FilteringExtension, FilteringState } from './extensions/FilteringExtension' -import { PolarView } from './extensions/CameraController' -import { InlineView } from './extensions/CameraController' -import { CanonicalView } from './extensions/CameraController' +import { Box3, MathUtils, Vector2 } from 'three' +import { + FilteringExtension, + type FilteringState +} from './extensions/FilteringExtension' +import { + type InlineView, + type PolarView, + type CanonicalView, + CameraController +} from './extensions/CameraController' import { SpeckleType } from './loaders/GeometryConverter' import { Queries } from './queries/Queries' -import { Query, QueryArgsResultMap, QueryResult } from './queries/Query' -import { DataTree, DataTreeBuilder } from './tree/DataTree' +import type { Query, QueryArgsResultMap, QueryResult } from './queries/Query' import { SelectionExtension, - SelectionExtensionOptions + type SelectionExtensionOptions } from './extensions/SelectionExtension' -import { StencilOutlineType } from '../IViewer' import { DefaultViewerParams, - IViewer, - SelectionEvent, - SpeckleView, - SunLightConfiguration, - ViewerParams + type IViewer, + type SelectionEvent, + type SpeckleView, + type SunLightConfiguration, + type ViewerParams, + StencilOutlineType } from '../IViewer' -import { TreeNode, WorldTree } from './tree/WorldTree' import { Viewer } from './Viewer' -import { CameraController } from './extensions/CameraController' import { SectionTool } from './extensions/SectionTool' import { SectionOutlines } from './extensions/SectionOutlines' +import { type TreeNode, WorldTree } from './tree/WorldTree' import { - MeasurementOptions, + type MeasurementOptions, MeasurementsExtension } from './extensions/measurements/MeasurementsExtension' import { ExplodeExtension } from './extensions/ExplodeExtension' -import { DiffExtension, DiffResult, VisualDiffMode } from './extensions/DiffExtension' -import { PropertyInfo } from './filtering/PropertyManager' +import { + DiffExtension, + type DiffResult, + VisualDiffMode +} from './extensions/DiffExtension' +import { type PropertyInfo } from './filtering/PropertyManager' import { BatchObject } from './batching/BatchObject' import { SpeckleLoader } from './loaders/Speckle/SpeckleLoader' +import Logger from 'js-logger' class LegacySelectionExtension extends SelectionExtension { /** FE2 'manually' selects objects pon it's own, so we're disabling the extension's event handler @@ -61,7 +70,7 @@ class HighlightExtension extends SelectionExtension { pointSize: 4 } } - this.setOptions(highlightMaterialData) + this.options = highlightMaterialData } public unselectObjects(ids: Array) { @@ -70,7 +79,8 @@ class HighlightExtension extends SelectionExtension { const nodes = [] for (let k = 0; k < ids.length; k++) { - nodes.push(...this.viewer.getWorldTree().findId(ids[k])) + const foundNodes = this.viewer.getWorldTree().findId(ids[k]) + if (foundNodes) nodes.push(...foundNodes) } this.clearSelection( nodes.filter((node: TreeNode) => { @@ -88,21 +98,20 @@ class HighlightExtension extends SelectionExtension { selection } - protected onPointerMove(e) { + protected onPointerMove(e: Vector2) { e } } export class LegacyViewer extends Viewer { - private cameraController: CameraController = null - private selection: SelectionExtension = null - private sections: SectionTool = null - private sectionOutlines: SectionOutlines = null - private measurements: MeasurementsExtension = null - private filtering: FilteringExtension = null - private explodeExtension: ExplodeExtension = null - private diffExtension: DiffExtension = null - private highlightExtension: HighlightExtension = null + private cameraController: CameraController + private selection: SelectionExtension + private sections: SectionTool + private measurements: MeasurementsExtension + private filtering: FilteringExtension + private explodeExtension: ExplodeExtension + private diffExtension: DiffExtension + private highlightExtension: HighlightExtension public constructor( container: HTMLElement, @@ -112,7 +121,7 @@ export class LegacyViewer extends Viewer { this.cameraController = this.createExtension(CameraController) this.selection = this.createExtension(LegacySelectionExtension) this.sections = this.createExtension(SectionTool) - this.sectionOutlines = this.createExtension(SectionOutlines) + this.createExtension(SectionOutlines) this.measurements = this.createExtension(MeasurementsExtension) this.filtering = this.createExtension(FilteringExtension) this.explodeExtension = this.createExtension(ExplodeExtension) @@ -143,7 +152,7 @@ export class LegacyViewer extends Viewer { if (!box) { box = this.speckleRenderer.sceneBox } - this.sections.setBox(box, offset) + this.sections.setBox(box as Box3, offset) } public getSectionBoxFromObjects(objectIds: string[]) { @@ -155,7 +164,7 @@ export class LegacyViewer extends Viewer { } public getCurrentSectionBox() { - return this.sections.getCurrentBox() + return this.sections.getBox() } public toggleSectionBox() { @@ -178,7 +187,7 @@ export class LegacyViewer extends Viewer { if (!this.filtering.filteringState.selectedObjects) this.filtering.filteringState.selectedObjects = [] this.filtering.filteringState.selectedObjects.push( - ...this.selection.getSelectedObjects().map((obj) => obj.id) + ...this.selection.getSelectedObjects().map((obj) => obj.id as string) ) return Promise.resolve(this.filtering.filteringState) } @@ -192,7 +201,7 @@ export class LegacyViewer extends Viewer { public hideObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false, ghost = false ): Promise { @@ -211,7 +220,7 @@ export class LegacyViewer extends Viewer { public showObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false ): Promise { return new Promise((resolve) => { @@ -224,7 +233,7 @@ export class LegacyViewer extends Viewer { public isolateObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false, ghost = true ): Promise { @@ -243,7 +252,7 @@ export class LegacyViewer extends Viewer { public unIsolateObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false ): Promise { return new Promise((resolve) => { @@ -294,7 +303,7 @@ export class LegacyViewer extends Viewer { }) } - public resetFilters(): Promise { + public resetFilters(): Promise { return new Promise((resolve) => { const filteringState = this.preserveSelectionFilter(() => { return this.filtering.resetFilters() @@ -303,20 +312,26 @@ export class LegacyViewer extends Viewer { }) } - private preserveSelectionFilter(filterFn: () => FilteringState): FilteringState { - const selectedObjects = this.selection.getSelectedObjects().map((obj) => obj.id) + private preserveSelectionFilter( + filterFn: () => FilteringState | null + ): FilteringState { + const selectedObjects = this.selection + .getSelectedObjects() + .map((obj) => obj.id) as string[] if (selectedObjects.length) this.selection.clearSelection() const filteringState = filterFn() - if (!filteringState.selectedObjects) - filteringState.selectedObjects = selectedObjects + if (filteringState) { + if (!filteringState.selectedObjects) + filteringState.selectedObjects = selectedObjects - this.selection.selectObjects(filteringState.selectedObjects) - return filteringState + this.selection.selectObjects(filteringState.selectedObjects) + } + return filteringState || this.filtering.filteringState } /** TREE */ - public getDataTree(): DataTree { - return DataTreeBuilder.build(this.tree) + public getDataTree(): void { + Logger.error('DataTree is obsolete, please use WorldTree instead') } public getWorldTree(): WorldTree { @@ -333,9 +348,10 @@ export class LegacyViewer extends Viewer { Queries.DefaultIntersectionQuerySolver.setContext(this.speckleRenderer) return Queries.DefaultIntersectionQuerySolver.solve(query) } + return null } - public queryAsync(query: Query): Promise { + public queryAsync(query: Query): Promise | null { //TO DO query return null @@ -391,11 +407,11 @@ export class LegacyViewer extends Viewer { return new Promise((resolve) => { const sectionBoxVisible = this.sections.enabled if (sectionBoxVisible) { - this.sections.displayOff() + this.sections.visible = false } const screenshot = this.speckleRenderer.renderer.domElement.toDataURL('image/png') if (sectionBoxVisible) { - this.sections.displayOn() + this.sections.visible = true } resolve(screenshot) }) @@ -407,11 +423,15 @@ export class LegacyViewer extends Viewer { public getObjects(id: string): BatchObject[] { const nodes = this.tree.findId(id) - const objects = [] - nodes.forEach((node: TreeNode) => { - if (node.model.renderView) - objects.push(this.speckleRenderer.getObject(node.model.renderView)) - }) + const objects: BatchObject[] = [] + if (nodes) { + nodes.forEach((node: TreeNode) => { + if (node.model.renderView) + objects.push( + this.speckleRenderer.getObject(node.model.renderView) as BatchObject + ) + }) + } return objects } @@ -421,7 +441,7 @@ export class LegacyViewer extends Viewer { public async loadObjectAsync( url: string, - token: string = null, + token: string | undefined = undefined, enableCaching = true, zoomToObject = true ) { @@ -442,11 +462,11 @@ export class LegacyViewer extends Viewer { return this.diffExtension.undiff() } - public setDiffTime(diffResult: DiffResult, time: number) { + public setDiffTime(_diffResult: DiffResult, time: number) { this.diffExtension.updateVisualDiff(time) } - public setVisualDiffMode(diffResult: DiffResult, mode: VisualDiffMode) { + public setVisualDiffMode(_diffResult: DiffResult, mode: VisualDiffMode) { this.diffExtension.updateVisualDiff(undefined, mode) } diff --git a/packages/viewer/src/modules/Shadowcatcher.ts b/packages/viewer/src/modules/Shadowcatcher.ts index 15dff06013..dbd1611077 100644 --- a/packages/viewer/src/modules/Shadowcatcher.ts +++ b/packages/viewer/src/modules/Shadowcatcher.ts @@ -12,23 +12,26 @@ import { Scene, Vector2, Vector3, - WebGLRenderer, ZeroFactor } from 'three' import { Geometry } from './converter/Geometry' import SpeckleBasicMaterial from './materials/SpeckleBasicMaterial' import { ShadowcatcherPass } from './pipeline/ShadowcatcherPass' import { ObjectLayers } from '../IViewer' -import { DefaultShadowcatcherConfig, ShadowcatcherConfig } from './ShadowcatcherConfig' +import { + DefaultShadowcatcherConfig, + type ShadowcatcherConfig +} from './ShadowcatcherConfig' +import type { SpeckleWebGLRenderer } from './objects/SpeckleWebGLRenderer' export class Shadowcatcher { public static readonly MESH_NAME = 'Shadowcatcher' public static readonly PLANE_SUBD = 2 public static readonly MAX_TEXTURE_SIZE_SCALE = 0.5 - private planeMesh: Mesh = null + private planeMesh: Mesh private planeSize: Vector2 = new Vector2() - private displayMaterial: SpeckleBasicMaterial = null - public shadowcatcherPass: ShadowcatcherPass = null + private displayMaterial: SpeckleBasicMaterial + public shadowcatcherPass: ShadowcatcherPass private _config: ShadowcatcherConfig = DefaultShadowcatcherConfig public get shadowcatcherMesh() { @@ -73,8 +76,8 @@ export class Shadowcatcher { this.shadowcatcherPass.update(scene) } - public render(renderer: WebGLRenderer) { - this.shadowcatcherPass.render(renderer, null, null) + public render(renderer: SpeckleWebGLRenderer) { + this.shadowcatcherPass.render(renderer) } public bake(worldBox: Box3, maxTexSize: number, force?: boolean) { diff --git a/packages/viewer/src/modules/SpeckleRenderer.ts b/packages/viewer/src/modules/SpeckleRenderer.ts index f9a95a52ad..9ac289a2aa 100644 --- a/packages/viewer/src/modules/SpeckleRenderer.ts +++ b/packages/viewer/src/modules/SpeckleRenderer.ts @@ -7,7 +7,7 @@ import { DirectionalLight, DirectionalLightHelper, Group, - Intersection, + type Intersection, Material, Mesh, Object3D, @@ -19,43 +19,53 @@ import { sRGBEncoding, Texture, Vector3, - VSMShadowMap + VSMShadowMap, + Vector2, + PerspectiveCamera, + OrthographicCamera } from 'three' -import { Batch, BatchUpdateRange, GeometryType } from './batching/Batch' +import { type Batch, type BatchUpdateRange, GeometryType } from './batching/Batch' import Batcher from './batching/Batcher' import { Geometry } from './converter/Geometry' -import Input, { InputEvent, InputOptionsDefault } from './input/Input' +import Input, { InputEvent } from './input/Input' import { Intersections } from './Intersections' import SpeckleDepthMaterial from './materials/SpeckleDepthMaterial' import SpeckleStandardMaterial from './materials/SpeckleStandardMaterial' import { NodeRenderView } from './tree/NodeRenderView' import { Viewer } from './Viewer' -import { TreeNode } from './tree/WorldTree' +import { WorldTree, type TreeNode } from './tree/WorldTree' import { DefaultLightConfiguration, ObjectLayers, - SelectionEvent, - SunLightConfiguration, + type SelectionEvent, + type SunLightConfiguration, ViewerEvent } from '../IViewer' -import { DefaultPipelineOptions, Pipeline, PipelineOptions } from './pipeline/Pipeline' +import { + DefaultPipelineOptions, + Pipeline, + type PipelineOptions +} from './pipeline/Pipeline' import { Shadowcatcher } from './Shadowcatcher' import SpeckleMesh from './objects/SpeckleMesh' -import { ExtendedIntersection } from './objects/SpeckleRaycaster' +import { type ExtendedIntersection } from './objects/SpeckleRaycaster' import { BatchObject } from './batching/BatchObject' -import { CameraEvent, SpeckleCamera } from './objects/SpeckleCamera' +import { CameraEvent, type SpeckleCamera } from './objects/SpeckleCamera' import Materials, { - RenderMaterial, - DisplayStyle, - FilterMaterial + type RenderMaterial, + type DisplayStyle, + type FilterMaterial, + type FilterMaterialOptions } from './materials/Materials' -import { MaterialOptions } from './materials/MaterialOptions' +import { type MaterialOptions } from './materials/MaterialOptions' import { SpeckleMaterial } from './materials/SpeckleMaterial' import { SpeckleWebGLRenderer } from './objects/SpeckleWebGLRenderer' import { SpeckleTypeAllRenderables } from './loaders/GeometryConverter' import SpeckleInstancedMesh from './objects/SpeckleInstancedMesh' import { BaseSpecklePass } from './pipeline/SpecklePass' import { MeshBatch } from './batching/MeshBatch' +import type { Pass } from 'three/examples/jsm/postprocessing/Pass.js' +import { RenderTree } from './tree/RenderTree' export class RenderingStats { private renderTimeAcc = 0 @@ -64,13 +74,13 @@ export class RenderingStats { private renderTimeStart = 0 public renderTime = 0 - public objects: number - public batchCount: number - public drawCalls: number - public trisCount: number - public vertCount: number + public objects: number = 0 + public batchCount: number = 0 + public drawCalls: number = 0 + public trisCount: number = 0 + public vertCount: number = 0 - public batchDetails: Array<{ + public batchDetails!: Array<{ drawCalls: number minDrawCalls: number tris: number @@ -93,32 +103,36 @@ export class RenderingStats { } export default class SpeckleRenderer { - private readonly SHOW_HELPERS = false - private readonly IGNORE_ZERO_OPACITY_OBJECTS = true + protected readonly SHOW_HELPERS = false + protected readonly IGNORE_ZERO_OPACITY_OBJECTS = true public SHOW_BVH = false - private container: HTMLElement - private _renderer: SpeckleWebGLRenderer - private _renderinStats: RenderingStats - public _scene: Scene - private _needsRender: boolean - private rootGroup: Group + + protected _renderer: SpeckleWebGLRenderer + protected _renderinStats: RenderingStats + protected _scene: Scene + protected _needsRender: boolean + protected _intersections: Intersections + protected _shadowcatcher: Shadowcatcher + protected _speckleCamera: SpeckleCamera | null = null + + protected container: HTMLElement + protected rootGroup: Group public batcher: Batcher - private _intersections: Intersections - public input: Input - private sun: DirectionalLight - private sunConfiguration: SunLightConfiguration = DefaultLightConfiguration - private sunTarget: Object3D - public viewer: Viewer // TEMPORARY - public pipeline: Pipeline + protected sun: DirectionalLight + protected sunConfiguration: SunLightConfiguration = DefaultLightConfiguration + protected sunTarget: Object3D + protected tree: WorldTree - private _shadowcatcher: Shadowcatcher = null - private cancel: { [subtreeId: string]: boolean } = {} + protected cancel: { [subtreeId: string]: boolean } = {} - private _speckleCamera: SpeckleCamera = null - private _clippingPlanes: Plane[] = [] - private _clippingVolume: Box3 = new Box3() + protected _clippingPlanes: Plane[] = [] + protected _clippingVolume: Box3 = new Box3() - private _renderOverride: () => void = null + protected _renderOverride: (() => void) | null = null + + public viewer: Viewer // TEMPORARY + public pipeline: Pipeline + public input: Input /******************************** * Renderer and rendering flags */ @@ -136,7 +150,7 @@ export default class SpeckleRenderer { /********************** * Bounds and volumes */ - public get sceneBox() { + public get sceneBox(): Box3 { const bounds: Box3 = new Box3() const batches = this.batcher.getBatches() for (let k = 0; k < batches.length; k++) { @@ -145,11 +159,11 @@ export default class SpeckleRenderer { return bounds } - public get sceneSphere() { + public get sceneSphere(): Sphere { return this.sceneBox.getBoundingSphere(new Sphere()) } - public get sceneCenter() { + public get sceneCenter(): Vector3 { return this.sceneBox.getCenter(new Vector3()) } @@ -176,12 +190,8 @@ export default class SpeckleRenderer { /**************** * Common Objects */ - public get allObjects() { - return this._scene.getObjectByName('ContentGroup') - } - - public subtree(subtreeId: string) { - return this._scene.getObjectByName(subtreeId) + public get allObjects(): Object3D { + return this._scene.getObjectByName('ContentGroup') as Object3D } public get scene() { @@ -191,7 +201,7 @@ export default class SpeckleRenderer { /******** * Lights */ - public get sunLight() { + public get sunLight(): DirectionalLight { return this.sun } @@ -213,7 +223,7 @@ export default class SpeckleRenderer { /******** * Camera */ - public get speckleCamera(): SpeckleCamera { + public get speckleCamera(): SpeckleCamera | null { return this._speckleCamera } @@ -235,7 +245,8 @@ export default class SpeckleRenderer { }) } - public get renderingCamera() { + public get renderingCamera(): PerspectiveCamera | OrthographicCamera | null { + if (!this._speckleCamera) return null return this._speckleCamera.renderingCamera } @@ -249,13 +260,13 @@ export default class SpeckleRenderer { return this.pipeline.pipelineOptions } - public get shadowcatcher() { + public get shadowcatcher(): Shadowcatcher | null { return this._shadowcatcher } /************** * Intersections */ - public get intersections() { + public get intersections(): Intersections { return this._intersections } @@ -294,7 +305,8 @@ export default class SpeckleRenderer { return this._renderinStats } - public constructor(viewer: Viewer /** TEMPORARY */) { + public constructor(tree: WorldTree, viewer: Viewer /** TEMPORARY */) { + this.tree = tree this._renderinStats = new RenderingStats() this._scene = new Scene() this.rootGroup = new Group() @@ -338,7 +350,7 @@ export default class SpeckleRenderer { this.pipeline.configure() this.pipeline.pipelineOptions = DefaultPipelineOptions - this.input = new Input(this._renderer.domElement, InputOptionsDefault) + this.input = new Input(this._renderer.domElement) this.input.on(InputEvent.Click, this.onClick.bind(this)) this.input.on(InputEvent.DoubleClick, this.onDoubleClick.bind(this)) @@ -368,7 +380,9 @@ export default class SpeckleRenderer { ObjectLayers.STREAM_CONTENT_MESH // ObjectLayers.STREAM_CONTENT_LINE ]) - let restoreVisibility, opaque + let restoreVisibility: Record + let opaque: Record + this._shadowcatcher.shadowcatcherPass.onBeforeRender = () => { restoreVisibility = this.batcher.saveVisiblity() opaque = this.batcher.getOpaque() @@ -387,7 +401,7 @@ export default class SpeckleRenderer { } public update(deltaTime: number) { - if (!this._speckleCamera) return + if (!this.renderingCamera) return this.batcher.update(deltaTime) this.renderingCamera.updateMatrixWorld(true) @@ -395,11 +409,11 @@ export default class SpeckleRenderer { this.updateRTEShadows() this.updateTransforms() - this.updateFrustum() + this.updateFrustum(this.renderingCamera) this.pipeline.update(this) - if (this.sunConfiguration.shadowcatcher) { + if (this.sunConfiguration.shadowcatcher && this._shadowcatcher) { this._shadowcatcher.update(this._scene) } } @@ -457,13 +471,12 @@ export default class SpeckleRenderer { private updateRTEShadows() { if (!this.updateRTEShadowBuffers()) return - const meshBatches = this.batcher.getBatches( + const meshBatches: MeshBatch[] = this.batcher.getBatches( undefined, GeometryType.MESH - ) as MeshBatch[] + ) for (let k = 0; k < meshBatches.length; k++) { - const speckleMesh: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k] - .renderObject as SpeckleMesh | SpeckleInstancedMesh + const speckleMesh: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k].mesh /** Shadowmap depth material does not go thorugh the normal flow. * It's onBeforeRender is not getting called That's why we're updating @@ -489,10 +502,12 @@ export default class SpeckleRenderer { } private updateTransforms() { - const meshBatches = this.batcher.getBatches(undefined, GeometryType.MESH) + const meshBatches: MeshBatch[] = this.batcher.getBatches( + undefined, + GeometryType.MESH + ) for (let k = 0; k < meshBatches.length; k++) { - const meshBatch: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k] - .renderObject as SpeckleMesh | SpeckleInstancedMesh + const meshBatch: SpeckleMesh | SpeckleInstancedMesh = meshBatches[k].mesh meshBatch.updateTransformsUniform() meshBatch.traverse((obj: Object3D) => { const depthMaterial: SpeckleDepthMaterial = @@ -504,10 +519,10 @@ export default class SpeckleRenderer { } } - private updateFrustum() { + private updateFrustum(camera: PerspectiveCamera | OrthographicCamera) { const v = new Vector3() const box = this.sceneBox - const camPos = new Vector3().copy(this.renderingCamera.position) + const camPos = new Vector3().copy(camera.position) let d = 0 v.set(box.min.x, box.min.y, box.min.z) // 000 d = Math.max(camPos.distanceTo(v), d) @@ -525,8 +540,8 @@ export default class SpeckleRenderer { d = Math.max(camPos.distanceTo(v), d) v.set(box.max.x, box.max.y, box.max.z) // 111 d = Math.max(camPos.distanceTo(v), d) - this.renderingCamera.far = d * 2 - this.renderingCamera.updateProjectionMatrix() + camera.far = d * 2 + camera.updateProjectionMatrix() } public resetPipeline() { @@ -549,7 +564,7 @@ export default class SpeckleRenderer { // this._needsRender = true this._renderinStats.frameEnd() - if (this.sunConfiguration.shadowcatcher) { + if (this.sunConfiguration.shadowcatcher && this._shadowcatcher) { this._shadowcatcher.render(this._renderer) } // console.log('Get transparent time -> ', InstancedMeshBatch.transparentTime) @@ -562,16 +577,16 @@ export default class SpeckleRenderer { this._needsRender = true } - public async *addRenderTree(subtreeId: string) { - this.cancel[subtreeId] = false + public async *addRenderTree(renderTree: RenderTree) { + this.cancel[renderTree.id] = false const subtreeGroup = new Group() - subtreeGroup.name = subtreeId + subtreeGroup.name = renderTree.id subtreeGroup.layers.set(ObjectLayers.STREAM_CONTENT) this.rootGroup.add(subtreeGroup) const generator = this.batcher.makeBatches( - this.viewer.getWorldTree(), - this.viewer.getWorldTree().getRenderTree(subtreeId), + this.tree, + renderTree, SpeckleTypeAllRenderables ) let currentBatchCount = 0 @@ -591,10 +606,10 @@ export default class SpeckleRenderer { this.updateDirectLights() } - if (this.cancel[subtreeId]) { + if (this.cancel[renderTree.id]) { generator.return() - this.removeRenderTree(subtreeId) - delete this.cancel[subtreeId] + this.removeRenderTree(renderTree.id) + delete this.cancel[renderTree.id] break } currentBatchCount++ @@ -607,8 +622,8 @@ export default class SpeckleRenderer { /** We'll just update the shadowcatcher after all batches are loaded */ this.updateShadowCatcher() this.updateClippingPlanes() - this._speckleCamera.setCameraPlanes(this.sceneBox) - delete this.cancel[subtreeId] + if (this._speckleCamera) this._speckleCamera.setCameraPlanes(this.sceneBox) + delete this.cancel[renderTree.id] } private addBatch(batch: Batch, parent: Object3D) { @@ -638,7 +653,7 @@ export default class SpeckleRenderer { } public removeRenderTree(subtreeId: string) { - this.rootGroup.remove(this.rootGroup.getObjectByName(subtreeId)) + this.rootGroup.remove(this.rootGroup.getObjectByName(subtreeId) as Object3D) this.updateShadowCatcher() const batches = this.batcher.getBatches(subtreeId) @@ -657,16 +672,22 @@ export default class SpeckleRenderer { } } - public setMaterial(rvs: NodeRenderView[], material: Material) + public setMaterial(rvs: NodeRenderView[], material: Material): void public setMaterial( rvs: NodeRenderView[], material: RenderMaterial & DisplayStyle & MaterialOptions - ) - public setMaterial(rvs: NodeRenderView[], material: FilterMaterial) - public setMaterial(rvs: NodeRenderView[], material) { + ): void + public setMaterial(rvs: NodeRenderView[], material: FilterMaterial): void + public setMaterial( + rvs: NodeRenderView[], + material: + | Material + | (RenderMaterial & DisplayStyle & MaterialOptions) + | FilterMaterial + ): void { if (!material) return - const rvMap = {} + const rvMap: { [id: string]: NodeRenderView[] } = {} for (let k = 0; k < rvs.length; k++) { if (!rvs[k].batchId) { continue @@ -699,7 +720,7 @@ export default class SpeckleRenderer { return { offset: value.batchStart, count: value.batchCount, material } }) if (this.batcher.batches[k]) - this.batcher.batches[k].setDrawRanges(...this.flattenDrawRanges(drawRanges)) + this.batcher.batches[k].setDrawRanges(this.flattenDrawRanges(drawRanges)) } } @@ -712,12 +733,17 @@ export default class SpeckleRenderer { return { offset: value.batchStart, count: value.batchCount, - material: this.batcher.materials.getFilterMaterial(value, material), - materialOptions: this.batcher.materials.getFilterMaterialOptions(material) + material: this.batcher.materials.getFilterMaterial( + value, + material + ) as Material, + materialOptions: this.batcher.materials.getFilterMaterialOptions( + material + ) as FilterMaterialOptions } }) if (this.batcher.batches[k]) - this.batcher.batches[k].setDrawRanges(...this.flattenDrawRanges(drawRanges)) + this.batcher.batches[k].setDrawRanges(this.flattenDrawRanges(drawRanges)) } } @@ -737,7 +763,7 @@ export default class SpeckleRenderer { }) if (this.batcher.batches[k]) - this.batcher.batches[k].setDrawRanges(...this.flattenDrawRanges(drawRanges)) + this.batcher.batches[k].setDrawRanges(this.flattenDrawRanges(drawRanges)) } } @@ -791,14 +817,14 @@ export default class SpeckleRenderer { return flatRanges } - public getMaterial(rv: NodeRenderView): Material { + public getMaterial(rv: NodeRenderView): Material | null { if (!rv || !rv.batchId) { return null } return this.batcher.getBatch(rv).getMaterial(rv) } - public getBatchMaterial(rv: NodeRenderView): Material { + public getBatchMaterial(rv: NodeRenderView): Material | null { if (!rv || !rv.batchId) { return null } @@ -818,7 +844,7 @@ export default class SpeckleRenderer { const planes = this._clippingPlanes this.allObjects.traverse((object) => { - const material = (object as unknown as { material }).material + const material = (object as unknown as { material: Material }).material if (!material) return if (!Array.isArray(material)) { material.clippingPlanes = planes @@ -829,13 +855,15 @@ export default class SpeckleRenderer { } }) this.pipeline.updateClippingPlanes(planes) - this._shadowcatcher.updateClippingPlanes(planes) + this._shadowcatcher?.updateClippingPlanes(planes) this.renderer.shadowMap.needsUpdate = true this.resetPipeline() } public updateShadowCatcher() { - this._shadowcatcher.shadowcatcherMesh.visible = this.sunConfiguration.shadowcatcher + if (this.sunConfiguration.shadowcatcher !== undefined) + this._shadowcatcher.shadowcatcherMesh.visible = + this.sunConfiguration.shadowcatcher if (this.sunConfiguration.shadowcatcher) { this._shadowcatcher.bake( this.clippingVolume, @@ -873,14 +901,17 @@ export default class SpeckleRenderer { this.sun.target = this.sunTarget } - public updateDirectLights() { + private updateDirectLights() { const phi = this.sunConfiguration.elevation const theta = this.sunConfiguration.azimuth - const radiusOffset = this.sunConfiguration.radius - this.sun.castShadow = this.sunConfiguration.castShadow - this.sun.intensity = this.sunConfiguration.intensity + const radiusOffset = this.sunConfiguration.radius || 0 + if (this.sunConfiguration.castShadow !== undefined) + this.sun.castShadow = this.sunConfiguration.castShadow + if (this.sunConfiguration.intensity !== undefined) + this.sun.intensity = this.sunConfiguration.intensity this.sun.color = new Color(this.sunConfiguration.color) - this.sun.visible = this.sunConfiguration.enabled + if (this.sunConfiguration.enabled !== undefined) + this.sun.visible = this.sunConfiguration.enabled this.sunTarget.position.copy(this.sceneCenter) const spherical = new Spherical(this.sceneSphere.radius + radiusOffset, phi, theta) @@ -946,7 +977,7 @@ export default class SpeckleRenderer { this.viewer.emit(ViewerEvent.LightConfigUpdated, { ...config }) } - public updateHelpers() { + private updateHelpers() { if (this.SHOW_HELPERS) { ;(this._scene.getObjectByName('CamHelper') as CameraHelper).update() // Thank you prettier, this looks so much better @@ -961,7 +992,7 @@ export default class SpeckleRenderer { public queryHits( results: Array - ): Array<{ node: TreeNode; point: Vector3 }> { + ): Array<{ node: TreeNode; point: Vector3 }> | null { const rvs = [] const points = [] for (let k = 0; k < results.length; k++) { @@ -981,7 +1012,10 @@ export default class SpeckleRenderer { for (let k = 0; k < rvs.length; k++) { const hitId = rvs[k].renderData.id const subtreeId = rvs[k].renderData.subtreeId - const hitNode = this.viewer.getWorldTree().findId(hitId, subtreeId)[0] + const hitNodes = this.tree.findId(hitId, subtreeId) + if (!hitNodes) continue + + const hitNode = hitNodes[0] let parentNode = hitNode while (!parentNode.model.atomic && parentNode.parent) { parentNode = parentNode.parent @@ -993,15 +1027,15 @@ export default class SpeckleRenderer { public queryHitIds( results: Array - ): Array<{ nodeId: string; point: Vector3 }> { + ): Array<{ nodeId: string; point: Vector3 }> | null { const queryResult = [] for (let k = 0; k < results.length; k++) { - let rv = results[k].batchObject?.renderView + let rv: NodeRenderView | null = results[k].batchObject + ?.renderView as NodeRenderView if (!rv) { - rv = this.batcher.getRenderView( - results[k].object.uuid, + const index: number | undefined = results[k].faceIndex !== undefined ? results[k].faceIndex : results[k].index - ) + if (index) rv = this.batcher.getRenderView(results[k].object.uuid, index) } if (rv) { queryResult.push({ nodeId: rv.renderData.id, point: results[k].point }) @@ -1016,42 +1050,47 @@ export default class SpeckleRenderer { return queryResult } + // TO DO: Maybe need a better way public renderViewFromIntersection( intersection: ExtendedIntersection - ): NodeRenderView { + ): NodeRenderView | null { let rv = null if (intersection.batchObject) { rv = intersection.batchObject.renderView const material = (intersection.object as SpeckleMesh).getBatchObjectMaterial( intersection.batchObject ) - if (material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) return null + if (material && material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) + return null } else { - rv = this.batcher.getRenderView( - intersection.object.uuid, + const index = intersection.faceIndex !== undefined ? intersection.faceIndex : intersection.index - ) - if (rv) { - const material = this.batcher.getRenderViewMaterial( - intersection.object.uuid, - intersection.faceIndex !== undefined - ? intersection.faceIndex - : intersection.index - ) - if (material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) return null + if (index) { + rv = this.batcher.getRenderView(intersection.object.uuid, index) + if (rv) { + const material = this.batcher.getRenderViewMaterial( + intersection.object.uuid, + index + ) + if (material && material.opacity === 0 && this.IGNORE_ZERO_OPACITY_OBJECTS) + return null + } } } return rv } - private onClick(e) { - const results: Array = this._intersections.intersect( + private onClick(e: Vector2 & { multiSelect: boolean; event: PointerEvent }) { + if (!this.renderingCamera) return + + const results: Array | null = this._intersections.intersect( this._scene, this.renderingCamera, e, + undefined, true, this.clippingVolume ) @@ -1085,11 +1124,14 @@ export default class SpeckleRenderer { this.viewer.emit(ViewerEvent.ObjectClicked, selectionInfo) } - private onDoubleClick(e) { - const results: Array = this._intersections.intersect( + private onDoubleClick(e: Vector2 & { multiSelect: boolean; event: PointerEvent }) { + if (!this.renderingCamera) return + + const results: Array | null = this._intersections.intersect( this._scene, this.renderingCamera, e, + undefined, true, this.clippingVolume ) @@ -1120,20 +1162,16 @@ export default class SpeckleRenderer { this.viewer.emit(ViewerEvent.ObjectDoubleClicked, selectionInfo) } - public boxFromObjects(objectIds: string[]) { + public boxFromObjects(objectIds: string[]): Box3 { let box = new Box3() const rvs: NodeRenderView[] = [] if (objectIds.length > 0) { for (let k = 0; k < objectIds.length; k++) { - const nodes = this.viewer.getWorldTree().findId(objectIds[k]) - nodes.forEach((node: TreeNode) => { - rvs.push( - ...this.viewer - .getWorldTree() - .getRenderTree() - .getRenderViewsForNode(node, node) - ) - }) + const nodes = this.tree.findId(objectIds[k]) + if (nodes) + nodes.forEach((node: TreeNode) => { + rvs.push(...this.tree.getRenderTree().getRenderViewsForNode(node)) + }) } } else box = this.sceneBox for (let k = 0; k < rvs.length; k++) { @@ -1215,7 +1253,7 @@ export default class SpeckleRenderer { return ids.reverse() } - public getBatchSize(batchId: string) { + public getBatchSize(batchId: string): number { return this.batcher.batches[batchId].renderViews.length } @@ -1225,23 +1263,25 @@ export default class SpeckleRenderer { } public getObjects(): BatchObject[] { - const batches = this.batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[] + const batches = this.batcher.getBatches(undefined, GeometryType.MESH) const meshes = batches.map((batch: MeshBatch) => batch.mesh) const objects = meshes.flatMap((mesh) => mesh.batchObjects) return objects } - public getObject(rv: NodeRenderView): BatchObject { + public getObject(rv: NodeRenderView): BatchObject | null { const batch = this.batcher.getBatch(rv) as MeshBatch if (batch.geometryType !== GeometryType.MESH) { // Logger.error('Render view is not of mesh type. No batch object found') return null } - return batch.mesh.batchObjects.find((value) => value.renderView.guid === rv.guid) + return batch.mesh.batchObjects.find( + (value) => value.renderView.guid === rv.guid + ) as BatchObject } public enableLayers(layers: ObjectLayers[], value: boolean) { - this.pipeline.composer.passes.forEach((pass: BaseSpecklePass) => { + this.pipeline.composer.passes.forEach((pass: Pass) => { if (!(pass instanceof BaseSpecklePass)) return layers.forEach((layer: ObjectLayers) => { pass.enableLayer(layer, value) diff --git a/packages/viewer/src/modules/UrlHelper.ts b/packages/viewer/src/modules/UrlHelper.ts index 4f407b576c..11d675170d 100644 --- a/packages/viewer/src/modules/UrlHelper.ts +++ b/packages/viewer/src/modules/UrlHelper.ts @@ -207,7 +207,9 @@ async function runModelLastVersionQuery( return `${ref.origin}/streams/${ref.projectId}/objects/${data.project.model.versions.items[0].referencedObject}` } catch (e) { Logger.error( - `Could not get object URLs for project ${ref.projectId} and model ${resource.modelId}. Error: ${e.message}` + `Could not get object URLs for project ${ref.projectId} and model ${ + resource.modelId + }. Error: ${e instanceof Error ? e.message : e}` ) } return '' @@ -245,7 +247,9 @@ async function runModelVersionQuery( return `${ref.origin}/streams/${ref.projectId}/objects/${data.project.model.version.referencedObject}` } catch (e) { Logger.error( - `Could not get object URLs for project ${ref.projectId} and model ${resource.modelId}. Error: ${e.message}` + `Could not get object URLs for project ${ref.projectId} and model ${ + resource.modelId + }. Error: ${e instanceof Error ? e.message : e}` ) } return '' @@ -292,7 +296,9 @@ async function runAllModelsQuery( return urls } catch (e) { Logger.error( - `Could not get object URLs for project ${ref.projectId}. Error: ${e.message}` + `Could not get object URLs for project ${ref.projectId}. Error: ${ + e instanceof Error ? e.message : e + }` ) } return [''] diff --git a/packages/viewer/src/modules/Viewer.ts b/packages/viewer/src/modules/Viewer.ts index 2cbcbb5295..5e00cde802 100644 --- a/packages/viewer/src/modules/Viewer.ts +++ b/packages/viewer/src/modules/Viewer.ts @@ -4,31 +4,32 @@ import EventEmitter from './EventEmitter' import { Clock, Texture } from 'three' import { Assets } from './Assets' -import { Optional } from '../helpers/typeHelper' +import { type Optional } from '../helpers/typeHelper' import { DefaultViewerParams, - IViewer, - SpeckleView, - SunLightConfiguration, + type IViewer, + type SpeckleView, + type SunLightConfiguration, UpdateFlags, ViewerEvent, - ViewerParams + type ViewerParams, + type ViewerEventPayload } from '../IViewer' import { World } from './World' -import { TreeNode, WorldTree } from './tree/WorldTree' +import { type TreeNode, WorldTree } from './tree/WorldTree' import SpeckleRenderer from './SpeckleRenderer' -import { PropertyInfo, PropertyManager } from './filtering/PropertyManager' -import { DataTree, DataTreeBuilder } from './tree/DataTree' +import { type PropertyInfo, PropertyManager } from './filtering/PropertyManager' import Logger from 'js-logger' -import { Query, QueryArgsResultMap, QueryResult } from './queries/Query' +import type { Query, QueryArgsResultMap } from './queries/Query' import { Queries } from './queries/Queries' -import { Utils } from './Utils' +import { type Utils } from './Utils' import { Extension } from './extensions/Extension' import Input from './input/Input' import { CameraController } from './extensions/CameraController' import { SpeckleType } from './loaders/GeometryConverter' import { Loader } from './loaders/Loader' import { type Constructor } from 'type-fest' +import { RenderTree } from './tree/RenderTree' export class Viewer extends EventEmitter implements IViewer { /** Container and optional stats element */ @@ -55,7 +56,7 @@ export class Viewer extends EventEmitter implements IViewer { } = {} /** various utils/helpers */ - protected utils: Utils + protected utils: Utils | undefined /** Gets the World object. Currently it's used for info mostly */ public get World(): World { return this.world @@ -87,21 +88,24 @@ export class Viewer extends EventEmitter implements IViewer { } public createExtension(type: Constructor): T { - const extensionsToInject: Array Extension> = - type.prototype.inject + const extensionsToInject: Array< + new (viewer: IViewer, ...args: Extension[]) => Extension + > = type.prototype.inject const injectedExtensions: Array = [] - extensionsToInject.forEach((value: new (viewer: IViewer, ...args) => Extension) => { - if (this.extensions[value.name]) { - injectedExtensions.push(this.extensions[value.name]) - return - } - for (const k in this.extensions) { - const prototypeChain = this.getConstructorChain(this.extensions[k]) - if (prototypeChain.includes(value.name)) { - injectedExtensions.push(this.extensions[k]) + extensionsToInject.forEach( + (value: new (viewer: IViewer, ...args: Extension[]) => Extension) => { + if (this.extensions[value.name]) { + injectedExtensions.push(this.extensions[value.name]) + return + } + for (const k in this.extensions) { + const prototypeChain = this.getConstructorChain(this.extensions[k]) + if (prototypeChain.includes(value.name)) { + injectedExtensions.push(this.extensions[k]) + } } } - }) + ) const extension = new type(this, ...injectedExtensions) this.extensions[type.name] = extension @@ -109,6 +113,18 @@ export class Viewer extends EventEmitter implements IViewer { } public getExtension(type: Constructor): T { + let extension + if ((extension = this.getExtensionInternal(type)) !== null) return extension + + throw new Error(`Could not get Extension of type ${type.name}. Is it created?`) + } + + public hasExtension(type: Constructor): boolean { + const extension = this.getExtensionInternal(type) + return extension ? true : false + } + + private getExtensionInternal(type: Constructor): T | null { if (this.extensions[type.name]) return this.extensions[type.name] as T else { for (const k in this.extensions) { @@ -118,6 +134,7 @@ export class Viewer extends EventEmitter implements IViewer { } } } + return null } public constructor( @@ -139,7 +156,7 @@ export class Viewer extends EventEmitter implements IViewer { this.clock = new Clock() this.inProgressOperations = 0 - this.speckleRenderer = new SpeckleRenderer(this) + this.speckleRenderer = new SpeckleRenderer(this.tree, this) this.speckleRenderer.create(this.container) window.addEventListener('resize', this.resize.bind(this), false) @@ -147,10 +164,6 @@ export class Viewer extends EventEmitter implements IViewer { this.frame() this.resize() - - this.on(ViewerEvent.LoadCancelled, (url: string) => { - Logger.warn(`Cancelled load for ${url}`) - }) } public getContainer() { @@ -170,7 +183,7 @@ export class Viewer extends EventEmitter implements IViewer { }) } - public requestRender(flags: number = UpdateFlags.RENDER) { + public requestRender(flags: UpdateFlags = UpdateFlags.RENDER) { if (flags & UpdateFlags.RENDER) { this.speckleRenderer.needsRender = true this.speckleRenderer.resetPipeline() @@ -225,26 +238,29 @@ export class Viewer extends EventEmitter implements IViewer { } } - public on(eventType: ViewerEvent, listener: (arg) => void): void { + on( + eventType: T, + listener: (arg: ViewerEventPayload[T]) => void + ): void { super.on(eventType, listener) } public getObjectProperties( - resourceURL: string = null, + resourceURL: string | null = null, bypassCache = true ): Promise { return this.propertyManager.getProperties(this.tree, resourceURL, bypassCache) } - public getDataTree(): DataTree { - return DataTreeBuilder.build(this.tree) + public getDataTree(): void { + Logger.error('DataTree has been deprecated! Please use WorldTree') } public getWorldTree(): WorldTree { return this.tree } - public query(query: T): QueryArgsResultMap[T['operation']] { + public query(query: T): QueryArgsResultMap[T['operation']] | null { if (Queries.isPointQuery(query)) { Queries.DefaultPointQuerySolver.setContext(this.speckleRenderer) return Queries.DefaultPointQuerySolver.solve(query) @@ -253,11 +269,7 @@ export class Viewer extends EventEmitter implements IViewer { Queries.DefaultIntersectionQuerySolver.setContext(this.speckleRenderer) return Queries.DefaultIntersectionQuerySolver.solve(query) } - } - public queryAsync(query: Query): Promise { - //TO DO - query return null } @@ -283,11 +295,11 @@ export class Viewer extends EventEmitter implements IViewer { return new Promise((resolve) => { // const sectionBoxVisible = this.sectionBox.display.visible // if (sectionBoxVisible) { - // this.sectionBox.displayOff() + // this.sectionBox.visible = false // } const screenshot = this.speckleRenderer.renderer.domElement.toDataURL('image/png') // if (sectionBoxVisible) { - // this.sectionBox.displayOn() + // this.sectionBox.visible = true // } resolve(screenshot) }) @@ -298,14 +310,20 @@ export class Viewer extends EventEmitter implements IViewer { */ public async loadObject(loader: Loader, zoomToObject = true) { - if (++this.inProgressOperations === 1) - (this as EventEmitter).emit(ViewerEvent.Busy, true) + if (++this.inProgressOperations === 1) this.emit(ViewerEvent.Busy, true) this.loaders[loader.resource] = loader const treeBuilt = await loader.load() if (treeBuilt) { + const renderTree: RenderTree | null = this.tree.getRenderTree(loader.resource) + /** Catering to typescript + * The render tree can't be null, we've just built it + */ + if (!renderTree) { + throw new Error(`Could not get render tree ${loader.resource}`) + } const t0 = performance.now() - for await (const step of this.speckleRenderer.addRenderTree(loader.resource)) { + for await (const step of this.speckleRenderer.addRenderTree(renderTree)) { step if (zoomToObject) { const extension = this.getExtension(CameraController) @@ -324,26 +342,23 @@ export class Viewer extends EventEmitter implements IViewer { if (this.loaders[loader.resource]) this.loaders[loader.resource].dispose() delete this.loaders[loader.resource] - if (--this.inProgressOperations === 0) - (this as EventEmitter).emit(ViewerEvent.Busy, false) + if (--this.inProgressOperations === 0) this.emit(ViewerEvent.Busy, false) } public async cancelLoad(resource: string, unload = false) { this.loaders[resource].cancel() - this.tree.getRenderTree(resource).cancelBuild(resource) + this.tree.getRenderTree(resource)?.cancelBuild() this.speckleRenderer.cancelRenderTree(resource) if (unload) { await this.unloadObject(resource) } else { - if (--this.inProgressOperations === 0) - (this as EventEmitter).emit(ViewerEvent.Busy, false) + if (--this.inProgressOperations === 0) this.emit(ViewerEvent.Busy, false) } } public async unloadObject(resource: string) { try { - if (++this.inProgressOperations === 1) - (this as EventEmitter).emit(ViewerEvent.Busy, true) + if (++this.inProgressOperations === 1) this.emit(ViewerEvent.Busy, true) if (this.tree.findSubtree(resource)) { if (this.loaders[resource]) { await this.cancelLoad(resource, true) @@ -351,38 +366,37 @@ export class Viewer extends EventEmitter implements IViewer { } delete this.loaders[resource] this.speckleRenderer.removeRenderTree(resource) - this.tree.getRenderTree(resource).purge() + this.tree.getRenderTree(resource)?.purge() this.tree.purge(resource) this.requestRender(UpdateFlags.RENDER | UpdateFlags.SHADOWS) } } finally { if (--this.inProgressOperations === 0) { - ;(this as EventEmitter).emit(ViewerEvent.Busy, false) + this.emit(ViewerEvent.Busy, false) Logger.warn(`Removed subtree ${resource}`) - ;(this as EventEmitter).emit(ViewerEvent.UnloadComplete, resource) + this.emit(ViewerEvent.UnloadComplete, resource) } } } public async unloadAll() { try { - if (++this.inProgressOperations === 1) - (this as EventEmitter).emit(ViewerEvent.Busy, true) + if (++this.inProgressOperations === 1) this.emit(ViewerEvent.Busy, true) for (const key of Object.keys(this.loaders)) { if (this.loaders[key]) await this.cancelLoad(key, false) delete this.loaders[key] } - this.tree.root.children.forEach((node) => { + this.tree.root.children.forEach((node: TreeNode) => { this.speckleRenderer.removeRenderTree(node.model.id) - this.tree.getRenderTree().purge() + this.tree.getRenderTree()?.purge() }) this.tree.purge() } finally { if (--this.inProgressOperations === 0) { - ;(this as EventEmitter).emit(ViewerEvent.Busy, false) + this.emit(ViewerEvent.Busy, false) Logger.warn(`Removed all subtrees`) - ;(this as EventEmitter).emit(ViewerEvent.UnloadAllComplete) + this.emit(ViewerEvent.UnloadAllComplete) } } } diff --git a/packages/viewer/src/modules/batching/Batch.ts b/packages/viewer/src/modules/batching/Batch.ts index c880ed9149..f95e2f6396 100644 --- a/packages/viewer/src/modules/batching/Batch.ts +++ b/packages/viewer/src/modules/batching/Batch.ts @@ -1,5 +1,5 @@ import { Box3, Material, Object3D, WebGLRenderer } from 'three' -import { FilterMaterialOptions } from '../materials/Materials' +import { type FilterMaterialOptions } from '../materials/Materials' import { NodeRenderView } from '../tree/NodeRenderView' export enum GeometryType { @@ -10,6 +10,12 @@ export enum GeometryType { TEXT } +export interface DrawGroup { + start: number + count: number + materialIndex: number +} + /** TO DO: Unify point and mesh batch implementations */ export interface Batch { id: string @@ -31,22 +37,22 @@ export interface Batch { getCount(): number setBatchMaterial(material: Material): void - setBatchBuffers(...range: BatchUpdateRange[]): void - setVisibleRange(...range: BatchUpdateRange[]) + setBatchBuffers(range: BatchUpdateRange[]): void + setVisibleRange(range: BatchUpdateRange[]): void getVisibleRange(): BatchUpdateRange - setDrawRanges(...ranges: BatchUpdateRange[]) - resetDrawRanges() - buildBatch() - getRenderView(index: number): NodeRenderView - getMaterialAtIndex(index: number): Material - getMaterial(rv: NodeRenderView): Material + setDrawRanges(ranges: BatchUpdateRange[]): void + resetDrawRanges(): void + buildBatch(): void + getRenderView(index: number): NodeRenderView | null + getMaterialAtIndex(index: number): Material | null + getMaterial(rv: NodeRenderView): Material | null getOpaque(): BatchUpdateRange getTransparent(): BatchUpdateRange getStencil(): BatchUpdateRange getDepth(): BatchUpdateRange - onUpdate(deltaTime: number) - onRender?(renderer: WebGLRenderer) - purge() + onUpdate(deltaTime?: number): void + onRender?(renderer: WebGLRenderer): void + purge(): void } export interface BatchUpdateRange { @@ -65,16 +71,6 @@ export const AllBatchUpdateRange = { offset: 0, count: Infinity } as BatchUpdateRange -export interface DrawGroup { - start: number - count: number - materialIndex?: number -} -export interface DrawGroup { - start: number - count: number - materialIndex?: number -} export const INSTANCE_TRANSFORM_BUFFER_STRIDE = 16 export const INSTANCE_GRADIENT_BUFFER_STRIDE = 1 diff --git a/packages/viewer/src/modules/batching/BatchObject.ts b/packages/viewer/src/modules/batching/BatchObject.ts index db612722cb..bc7c7b4337 100644 --- a/packages/viewer/src/modules/batching/BatchObject.ts +++ b/packages/viewer/src/modules/batching/BatchObject.ts @@ -12,6 +12,8 @@ export type VectorLike = | { x: number; y: number; z?: number; w?: number } | undefined | null +export type Vector3Like = VectorLike & { z: number } +export type Vector4Like = Vector3Like & { w: number } export class BatchObject { protected _renderView: NodeRenderView @@ -21,8 +23,8 @@ export class BatchObject { public transform: Matrix4 public transformInv: Matrix4 - public tasVertIndexStart: number - public tasVertIndexEnd: number + public tasVertIndexStart!: number + public tasVertIndexEnd!: number public quaternion: Quaternion = new Quaternion() public eulerValue: Euler = new Euler() @@ -53,14 +55,13 @@ export class BatchObject { return this._batchIndex } - public get speckleId(): string { - return this._renderView.renderData.id - } - public get aabb(): Box3 { - const box = new Box3().copy(this.renderView.aabb) - box.applyMatrix4(this.transform) - return box + if (this.renderView.aabb) { + const box = new Box3().copy(this.renderView.aabb) + box.applyMatrix4(this.transform) + return box + } + return new Box3() } public get localOrigin(): Vector3 { @@ -117,11 +118,13 @@ export class BatchObject { transform.invert() if (!bvh) { - const indices = this._renderView.renderData.geometry.attributes.INDEX - const position = this._renderView.renderData.geometry.attributes.POSITION + const indices: number[] | undefined = + this._renderView.renderData.geometry.attributes?.INDEX + const position: number[] | undefined = + this._renderView.renderData.geometry.attributes?.POSITION bvh = AccelerationStructure.buildBVH( indices, - new Float32Array(position), + position, DefaultBVHOptions, transform ) @@ -137,10 +140,10 @@ export class BatchObject { } public transformTRS( - translation: VectorLike, - euler: VectorLike, - scale: VectorLike, - pivot: VectorLike + translation: Vector3Like, + euler?: Vector3Like, + scale?: Vector3Like, + pivot?: Vector3Like ) { let T: Matrix4 = BatchObject.matBuff0.identity() let R: Matrix4 = BatchObject.matBuff1.identity() diff --git a/packages/viewer/src/modules/batching/Batcher.ts b/packages/viewer/src/modules/batching/Batcher.ts index 1085c95aff..772ed53793 100644 --- a/packages/viewer/src/modules/batching/Batcher.ts +++ b/packages/viewer/src/modules/batching/Batcher.ts @@ -1,8 +1,17 @@ import { MathUtils } from 'three' import LineBatch from './LineBatch' -import Materials, { FilterMaterialType } from '../materials/Materials' +import Materials, { + FilterMaterialType, + type DisplayStyle, + type RenderMaterial +} from '../materials/Materials' import { NodeRenderView } from '../tree/NodeRenderView' -import { Batch, BatchUpdateRange, GeometryType, NoneBatchUpdateRange } from './Batch' +import { + type Batch, + type BatchUpdateRange, + GeometryType, + NoneBatchUpdateRange +} from './Batch' import { Material, WebGLRenderer } from 'three' import Logger from 'js-logger' import { AsyncPause } from '../World' @@ -10,12 +19,20 @@ import { RenderTree } from '../tree/RenderTree' import TextBatch from './TextBatch' import SpeckleMesh, { TransformStorage } from '../objects/SpeckleMesh' import { SpeckleType } from '../loaders/GeometryConverter' -import { TreeNode, WorldTree } from '../..' +import { type TreeNode, WorldTree } from '../..' import { InstancedMeshBatch } from './InstancedMeshBatch' import { Geometry } from '../converter/Geometry' import { MeshBatch } from './MeshBatch' import { PointBatch } from './PointBatch' +type BatchTypeMap = { + [GeometryType.MESH]: MeshBatch + [GeometryType.LINE]: LineBatch + [GeometryType.POINT]: PointBatch + [GeometryType.POINT_CLOUD]: PointBatch + [GeometryType.TEXT]: TextBatch +} + export default class Batcher { private maxHardwareUniformCount = 0 private floatTextures = false @@ -41,7 +58,6 @@ export default class Batcher { speckleType: SpeckleType[], batchType?: GeometryType ) { - const start = performance.now() let min = Number.MAX_SAFE_INTEGER, max = -1, average = 0, @@ -51,7 +67,6 @@ export default class Batcher { const instancedBatches: { [id: string]: Array } = {} const pause = new AsyncPause() - const startInstancedGathering = performance.now() for (const g in instanceGroups) { pause.tick(100) if (pause.needsWait) { @@ -81,13 +96,10 @@ export default class Batcher { } instancedBatches[vertCount].push(g) } - const instancedGathering = performance.now() - startInstancedGathering - - let deInstancing = 0 - let instanceBuild = 0 for (const v in instancedBatches) { for (let k = 0; k < instancedBatches[v].length; k++) { const nodes = worldTree.findId(instancedBatches[v][k]) + if (!nodes) continue /** Make sure entire instance set is instanced */ let instanced = true nodes.every((node: TreeNode) => (instanced &&= node.model.instanced)) @@ -98,7 +110,6 @@ export default class Batcher { .filter((rv) => rv) if (Number.parseInt(v) < this.minInstancedBatchVertices || !instanced) { - const t0 = performance.now() rvs.forEach((nodeRv) => { const geometry = nodeRv.renderData.geometry geometry.instanced = false @@ -118,28 +129,24 @@ export default class Batcher { Geometry.transformGeometryData(geometry, geometry.transform) nodeRv.computeAABB() }) - deInstancing += performance.now() - t0 continue } - const t1 = performance.now() const materialHash = rvs[0].renderMaterialHash const instancedBatch = await this.buildInstancedBatch( renderTree, rvs, materialHash ) - instanceBuild += performance.now() - t1 + if (!instancedBatch) continue this.batches[instancedBatch.id] = instancedBatch min = Math.min(min, instancedBatch.renderViews.length) max = Math.max(max, instancedBatch.renderViews.length) - average += instancedBatch.renderViews.length batchCount++ yield this.batches[instancedBatch.id] } } - const totalInstanced = performance.now() - start const renderViews = renderTree .getRenderableNodes(...speckleType) @@ -193,6 +200,8 @@ export default class Batcher { batchType ) + if (!batch) continue + this.batches[batch.id] = batch min = Math.min(min, batch.renderViews.length) max = Math.max(max, batch.renderViews.length) @@ -206,10 +215,6 @@ export default class Batcher { average / materialHashes.length }` ) - Logger.warn('Total instanced -> ', totalInstanced) - Logger.warn('Instance gathering -> ', instancedGathering) - Logger.warn('De-instancing -> ', deInstancing) - Logger.warn('Instanced build -> ', instanceBuild) } private splitBatch( @@ -217,19 +222,29 @@ export default class Batcher { vertCount: number ): NodeRenderView[][] { /** We're first splitting based on the batch's max vertex count */ - const vSplit = [] + const vSplit: Array> = [] const vDiv = Math.floor(vertCount / this.maxBatchVertices) if (vDiv > 0) { let count = 0 let index = 0 vSplit.push([]) for (let k = 0; k < renderViews.length; k++) { + /** Catering to typescript. + * RenderViews are prefiltered based on valid geometry before reaching this point + */ + const ervee = renderViews[k] + const nextErvee = renderViews[k + 1] + if (!ervee.renderData.geometry.attributes) { + throw new Error( + `Invalid geometry on render view ${renderViews[k].renderData.id}` + ) + } vSplit[index].push(renderViews[k]) - count += renderViews[k].renderData.geometry.attributes.POSITION.length / 3 + count += ervee.renderData.geometry.attributes.POSITION.length / 3 const nexCount = count + - (renderViews[k + 1] - ? renderViews[k + 1].renderData.geometry.attributes.POSITION.length / 3 + (nextErvee && nextErvee.renderData.geometry.attributes + ? nextErvee.renderData.geometry.attributes.POSITION.length / 3 : 0) if (nexCount >= this.maxBatchVertices && renderViews[k + 1]) { vSplit.push([]) @@ -267,7 +282,7 @@ export default class Batcher { renderTree: RenderTree, renderViews: NodeRenderView[], materialHash: number - ): Promise { + ): Promise { if (!renderViews.length) { /** This is for the case when all renderviews have invalid geometries, and it generally * means there is something wrong with the stream @@ -294,7 +309,7 @@ export default class Batcher { renderViews: NodeRenderView[], materialHash: number, batchType?: GeometryType - ): Promise { + ): Promise { if (!renderViews.length) { /** This is for the case when all renderviews have invalid geometries, and it generally * means there is something wrong with the stream @@ -308,7 +323,8 @@ export default class Batcher { const geometryType = batchType !== undefined ? batchType : renderViews[0].geometryType - let matRef = null + let matRef: RenderMaterial | DisplayStyle | null = + renderViews[0].renderData.renderMaterial if (geometryType === GeometryType.MESH) { matRef = renderViews[0].renderData.renderMaterial @@ -325,7 +341,7 @@ export default class Batcher { const material = this.materials.getMaterial(materialHash, matRef, geometryType) const batchID = MathUtils.generateUUID() - let geometryBatch: Batch = null + let geometryBatch: Batch | null = null switch (geometryType) { case GeometryType.MESH: geometryBatch = new MeshBatch( @@ -365,12 +381,13 @@ export default class Batcher { public render(renderer: WebGLRenderer) { for (const batchId in this.batches) { - if (this.batches[batchId].onRender) this.batches[batchId].onRender(renderer) + const batch = this.batches[batchId] + if (batch.onRender) batch.onRender(renderer) } } public saveVisiblity(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { const batch: Batch = this.batches[k] visibilityRanges[k] = batch.getVisibleRange() @@ -383,15 +400,15 @@ export default class Batcher { const batch: Batch = this.batches[k] const range = ranges[k] if (!range) { - batch.setVisibleRange(NoneBatchUpdateRange) + batch.setVisibleRange([NoneBatchUpdateRange]) } else { - batch.setVisibleRange(range) + batch.setVisibleRange([range]) } } } public getTransparent(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { visibilityRanges[k] = this.batches[k].getTransparent() } @@ -399,7 +416,7 @@ export default class Batcher { } public getStencil(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { visibilityRanges[k] = this.batches[k].getStencil() } @@ -407,7 +424,7 @@ export default class Batcher { } public getOpaque(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { visibilityRanges[k] = this.batches[k].getOpaque() } @@ -415,7 +432,7 @@ export default class Batcher { } public getDepth(): Record { - const visibilityRanges = {} + const visibilityRanges: Record = {} for (const k in this.batches) { visibilityRanges[k] = this.batches[k].getDepth() } @@ -450,13 +467,38 @@ export default class Batcher { } } - public getBatches(subtreeId?: string, geometryType?: GeometryType) { - return Object.values(this.batches).filter((value: Batch) => { + public getBatches( + subtreeId?: string, + geometryType?: K + ): BatchTypeMap[K][] { + const batches: Batch[] = Object.values(this.batches) + return batches.filter((value: Batch) => { const subtree = subtreeId !== undefined ? value.subtreeId === subtreeId : true const type = - geometryType !== undefined ? value.geometryType === geometryType : true + geometryType !== undefined ? this.isBatchType(value, geometryType) : true return subtree && type - }) + }) as BatchTypeMap[K][] + } + + private isBatchType( + batch: Batch, + geometryType?: K + ): batch is BatchTypeMap[K] { + if (geometryType === undefined) return true + switch (geometryType) { + case GeometryType.MESH: + return batch instanceof MeshBatch + case GeometryType.LINE: + return batch instanceof LineBatch + case GeometryType.POINT: + return batch instanceof PointBatch + case GeometryType.POINT_CLOUD: + return batch instanceof PointBatch + case GeometryType.TEXT: + return batch instanceof TextBatch + default: + return false + } } public getBatch(rv: NodeRenderView) { @@ -490,32 +532,18 @@ export default class Batcher { /** * Used for debuggin only */ - - public async isolateRenderViewBatch(id: string, renderTree: RenderTree) { - const rv = renderTree.getRenderViewForNodeId(id) - for (const k in this.batches) { - if (k !== rv.batchId) { - this.batches[k].setDrawRanges({ - offset: 0, - count: this.batches[k].getCount(), - material: this.materials.getFilterMaterial(this.batches[k].renderViews[0], { - filterType: FilterMaterialType.GHOST - }) - }) - } - } - } - public async isolateBatch(batchId: string) { for (const k in this.batches) { if (k !== batchId) { - this.batches[k].setDrawRanges({ - offset: 0, - count: this.batches[k].getCount(), - material: this.materials.getFilterMaterial(this.batches[k].renderViews[0], { - filterType: FilterMaterialType.GHOST - }) - }) + this.batches[k].setDrawRanges([ + { + offset: 0, + count: this.batches[k].getCount(), + material: this.materials.getFilterMaterial(this.batches[k].renderViews[0], { + filterType: FilterMaterialType.GHOST + }) as Material + } + ]) } } } diff --git a/packages/viewer/src/modules/batching/DrawRanges.ts b/packages/viewer/src/modules/batching/DrawRanges.ts index 219030dedc..17f2979165 100644 --- a/packages/viewer/src/modules/batching/DrawRanges.ts +++ b/packages/viewer/src/modules/batching/DrawRanges.ts @@ -1,6 +1,6 @@ import { Material } from 'three' -import { BatchUpdateRange } from './Batch' -import { DrawGroup } from './Batch' +import { type BatchUpdateRange } from './Batch' +import { type DrawGroup } from './Batch' export class DrawRanges { public integrateRanges( @@ -12,14 +12,14 @@ export class DrawRanges { groups.sort((a, b) => a.start - b.start) ranges.sort((a, b) => a.offset - b.offset) - const edgesForward = {} - const edgesBackwards = {} + const edgesForward: { [key: number]: number } = {} + const edgesBackwards: { [key: number]: number } = {} for (let k = 0, l = groups.length - 1; k < groups.length; k++, l--) { const groupForward = groups[k] const groupBackwards = groups[l] - edgesForward[groupForward.start] = groupForward.materialIndex + edgesForward[groupForward.start] = groupForward.materialIndex as number edgesBackwards[groupBackwards.start + groupBackwards.count] = - groupBackwards.materialIndex + groupBackwards.materialIndex as number } _flatRanges = groups.map((group: DrawGroup) => { @@ -43,7 +43,7 @@ export class DrawRanges { }) _flatRanges = [...new Set(_flatRanges)] - const materialIndex = materials.indexOf(range.material) + const materialIndex = materials.indexOf(range.material as Material) edgesForward[r0] = materialIndex edgesForward[r1] = r1 >= next ? edgesForward[next] : edgesBackwards[next] } diff --git a/packages/viewer/src/modules/batching/InstancedBatchObject.ts b/packages/viewer/src/modules/batching/InstancedBatchObject.ts index ec8b17ef12..f599d27d18 100644 --- a/packages/viewer/src/modules/batching/InstancedBatchObject.ts +++ b/packages/viewer/src/modules/batching/InstancedBatchObject.ts @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import { BatchObject, VectorLike } from './BatchObject' +import { BatchObject, type Vector3Like } from './BatchObject' import { Matrix4 } from 'three' import { NodeRenderView } from '../tree/NodeRenderView' @@ -9,17 +9,18 @@ export class InstancedBatchObject extends BatchObject { public constructor(renderView: NodeRenderView, batchIndex: number) { super(renderView, batchIndex) - this.instanceTransform.copy(renderView.renderData.geometry.transform) + if (renderView.renderData.geometry.transform) + this.instanceTransform.copy(renderView.renderData.geometry.transform) this.transform.copy(this.instanceTransform) this.transformInv.copy(new Matrix4().copy(this.instanceTransform).invert()) this.transformDirty = false } public transformTRS( - translation: VectorLike, - euler: VectorLike, - scale: VectorLike, - pivot: VectorLike + translation: Vector3Like, + euler: Vector3Like, + scale: Vector3Like, + pivot: Vector3Like ) { super.transformTRS(translation, euler, scale, pivot) this.transform.multiply(this.instanceTransform) diff --git a/packages/viewer/src/modules/batching/InstancedMeshBatch.ts b/packages/viewer/src/modules/batching/InstancedMeshBatch.ts index 736b7a2715..f283d74e95 100644 --- a/packages/viewer/src/modules/batching/InstancedMeshBatch.ts +++ b/packages/viewer/src/modules/batching/InstancedMeshBatch.ts @@ -14,8 +14,9 @@ import { Geometry } from '../converter/Geometry' import { NodeRenderView } from '../tree/NodeRenderView' import { AllBatchUpdateRange, - Batch, - BatchUpdateRange, + type Batch, + type BatchUpdateRange, + type DrawGroup, GeometryType, INSTANCE_TRANSFORM_BUFFER_STRIDE, NoneBatchUpdateRange @@ -31,7 +32,6 @@ import Logger from 'js-logger' import Materials from '../materials/Materials' import { DrawRanges } from './DrawRanges' import SpeckleStandardColoredMaterial from '../materials/SpeckleStandardColoredMaterial' -import { DrawGroup } from './Batch' export class InstancedMeshBatch implements Batch { public id: string @@ -40,12 +40,12 @@ export class InstancedMeshBatch implements Batch { private geometry: BufferGeometry public batchMaterial: Material public mesh: SpeckleInstancedMesh - private drawRanges: DrawRanges = new DrawRanges() + protected drawRanges: DrawRanges = new DrawRanges() - private instanceTransformBuffer0: Float32Array = null - private instanceTransformBuffer1: Float32Array = null + private instanceTransformBuffer0!: Float32Array + private instanceTransformBuffer1!: Float32Array private transformBufferIndex: number = 0 - private instanceGradientBuffer: Float32Array = null + private instanceGradientBuffer!: Float32Array private needsShuffle = false @@ -67,7 +67,11 @@ export class InstancedMeshBatch implements Batch { } public get triCount(): number { - return (this.geometry.index.count / 3) * this.renderViews.length + /** Catering to typescript + * There is no unniverse where the geometry is non-indexed. We're **explicitly** setting the index at creation time + */ + const indexCount = this.geometry.index ? this.geometry.index.count : 0 + return (indexCount / 3) * this.renderViews.length } public get vertCount(): number { @@ -125,7 +129,7 @@ export class InstancedMeshBatch implements Batch { } /** Note: You can only set visibility on ranges that exist as draw groups! */ - public setVisibleRange(...ranges: BatchUpdateRange[]) { + public setVisibleRange(ranges: BatchUpdateRange[]) { /** Entire batch needs to NOT be drawn */ if (ranges.length === 1 && ranges[0] === NoneBatchUpdateRange) { this.mesh.children.forEach((instance) => (instance.visible = false)) @@ -139,14 +143,15 @@ export class InstancedMeshBatch implements Batch { this.mesh.children.forEach((instance) => (instance.visible = false)) ranges.forEach((range) => { - const instanceIndex = this.groups.indexOf( - this.groups.find( - (group: DrawGroup) => - range.offset === group.start && - range.offset + range.count === group.start + group.count - ) + const foundInstance = this.groups.find( + (group: DrawGroup) => + range.offset === group.start && + range.offset + range.count === group.start + group.count ) - if (instanceIndex !== -1) this.mesh.children[instanceIndex].visible = true + if (foundInstance) { + const instanceIndex = this.groups.indexOf(foundInstance) + if (instanceIndex !== -1) this.mesh.children[instanceIndex].visible = true + } }) } @@ -166,6 +171,7 @@ export class InstancedMeshBatch implements Batch { public getOpaque(): BatchUpdateRange { /** If there is any transparent or hidden group return the update range up to it's offset */ const transparentOrHiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return ( Materials.isTransparent(this.materials[value.materialIndex]) || this.materials[value.materialIndex].visible === false @@ -185,6 +191,7 @@ export class InstancedMeshBatch implements Batch { public getDepth(): BatchUpdateRange { /** If there is any transparent or hidden group return the update range up to it's offset */ const transparentOrHiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return ( Materials.isTransparent(this.materials[value.materialIndex]) || this.materials[value.materialIndex].visible === false || @@ -205,10 +212,12 @@ export class InstancedMeshBatch implements Batch { public getTransparent(): BatchUpdateRange { /** Look for a transparent group */ const transparentGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return Materials.isTransparent(this.materials[value.materialIndex]) }) /** Look for a hidden group */ const hiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].visible === false }) /** If there is a transparent group return it's range */ @@ -234,6 +243,7 @@ export class InstancedMeshBatch implements Batch { if (this.materials[0].stencilWrite === true) return AllBatchUpdateRange } const stencilGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].stencilWrite === true }) if (stencilGroup) { @@ -246,28 +256,33 @@ export class InstancedMeshBatch implements Batch { return NoneBatchUpdateRange } - public setBatchBuffers(...range: BatchUpdateRange[]): void { - for (let k = 0; k < range.length; k++) { - if (range[k].materialOptions) { - if (range[k].materialOptions.rampIndex !== undefined) { - const start = range[k].offset + public setBatchBuffers(ranges: BatchUpdateRange[]): void { + for (let k = 0; k < ranges.length; k++) { + const range = ranges[k] + if (range.materialOptions) { + if ( + range.materialOptions.rampIndex !== undefined && + range.materialOptions.rampWidth !== undefined + ) { + const start = ranges[k].offset /** The ramp indices specify the *begining* of each ramp color. When sampling with Nearest filter (since we don't want filtering) * we'll always be sampling right at the edge between texels. Most GPUs will sample consistently, but some won't and we end up with * a ton of artifacts. To avoid this, we are shifting the sampling indices so they're right on the center of each texel, so no inconsistent * sampling can occur. */ - const shiftedIndex = - range[k].materialOptions.rampIndex + - 0.5 / range[k].materialOptions.rampWidth - this.updateGradientIndexBufferData(start / 16, shiftedIndex) + if (range.materialOptions.rampIndex && range.materialOptions.rampWidth) { + const shiftedIndex = + range.materialOptions.rampIndex + 0.5 / range.materialOptions.rampWidth + this.updateGradientIndexBufferData(start / 16, shiftedIndex) + } } /** We need to update the texture here, because each batch uses it's own clone for any material we use on it * because otherwise three.js won't properly update our custom uniforms */ - if (range[k].materialOptions.rampTexture !== undefined) { - if (range[k].material instanceof SpeckleStandardColoredMaterial) { - ;(range[k].material as SpeckleStandardColoredMaterial).setGradientTexture( - range[k].materialOptions.rampTexture + if (range.materialOptions.rampTexture !== undefined) { + if (range.material instanceof SpeckleStandardColoredMaterial) { + ;(range.material as SpeckleStandardColoredMaterial).setGradientTexture( + range.materialOptions.rampTexture ) } } @@ -275,15 +290,15 @@ export class InstancedMeshBatch implements Batch { } } - public setDrawRanges(...ranges: BatchUpdateRange[]) { + public setDrawRanges(ranges: BatchUpdateRange[]) { ranges.forEach((value: BatchUpdateRange) => { if (value.material) { value.material = this.mesh.getCachedMaterial(value.material) } }) - const materials = ranges.map((val) => { - return val.material + const materials: Array = ranges.map((val: BatchUpdateRange) => { + return val.material as Material }) const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))] @@ -303,7 +318,7 @@ export class InstancedMeshBatch implements Batch { if (count !== this.renderViews.length * 16) { Logger.error(`Draw groups invalid on ${this.id}`) } - this.setBatchBuffers(...ranges) + this.setBatchBuffers(ranges) this.cleanMaterials() /** We shuffle only when above a certain fragmentation threshold. We don't want to be shuffling every single time */ if (this.drawCalls > this.maxDrawCalls) { @@ -335,7 +350,7 @@ export class InstancedMeshBatch implements Batch { } } - private shuffleDrawGroups() { + private shuffleDrawGroups(): void { const groups = this.groups .sort((a, b) => { return a.start - b.start @@ -354,10 +369,12 @@ export class InstancedMeshBatch implements Batch { return transparentOrder }) - const materialOrder = [] + const materialOrder: Array = [] groups.reduce((previousValue, currentValue) => { - if (previousValue.indexOf(currentValue.materialIndex) === -1) { - previousValue.push(currentValue.materialIndex) + if (currentValue.materialIndex !== undefined) { + if (previousValue.indexOf(currentValue.materialIndex) === -1) { + previousValue.push(currentValue.materialIndex) + } } return previousValue }, materialOrder) @@ -438,17 +455,20 @@ export class InstancedMeshBatch implements Batch { /** Solve hidden groups */ const hiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].visible === false }) if (hiddenGroup) { - this.setVisibleRange({ - offset: 0, - count: hiddenGroup.start - }) + this.setVisibleRange([ + { + offset: 0, + count: hiddenGroup.start + } + ]) } } - public resetDrawRanges() { + public resetDrawRanges(): void { this.groups.length = 0 this.materials.length = 0 this.groups.push({ @@ -457,7 +477,7 @@ export class InstancedMeshBatch implements Batch { materialIndex: 0 }) this.materials.push(this.batchMaterial) - this.setVisibleRange(AllBatchUpdateRange) + this.setVisibleRange([AllBatchUpdateRange]) this.mesh.updateDrawGroups( this.getCurrentTransformBuffer(), this.getCurrentGradientBuffer() @@ -480,7 +500,7 @@ export class InstancedMeshBatch implements Batch { return this.instanceGradientBuffer } - public buildBatch() { + public buildBatch(): void { const batchObjects = [] let instanceBVH = null this.instanceTransformBuffer0 = new Float32Array( @@ -492,7 +512,17 @@ export class InstancedMeshBatch implements Batch { const targetInstanceTransformBuffer = this.getCurrentTransformBuffer() for (let k = 0; k < this.renderViews.length; k++) { - this.renderViews[k].renderData.geometry.transform.toArray( + /** Catering to typescript + * There is no unniverse where an instanced render view does not have a transform + * It's against it's definition + */ + const ervee = this.renderViews[k] + if (!ervee.renderData.geometry.transform) { + throw new Error( + `Instanced Render view with id ${ervee.renderData.id} has null transform!` + ) + } + ervee.renderData.geometry.transform.toArray( targetInstanceTransformBuffer, k * INSTANCE_TRANSFORM_BUFFER_STRIDE ) @@ -509,11 +539,13 @@ export class InstancedMeshBatch implements Batch { batchObject.localOrigin.z ) transform.invert() - const indices = this.renderViews[k].renderData.geometry.attributes.INDEX - const position = this.renderViews[k].renderData.geometry.attributes.POSITION + const indices: number[] | undefined = + this.renderViews[k].renderData.geometry.attributes?.INDEX + const position: number[] | undefined = this.renderViews[k].renderData.geometry + .attributes?.POSITION as number[] instanceBVH = AccelerationStructure.buildBVH( indices, - new Float32Array(position), + position, DefaultBVHOptions, transform ) @@ -524,25 +556,32 @@ export class InstancedMeshBatch implements Batch { batchObjects.push(batchObject) } - const indices = new Uint32Array( - this.renderViews[0].renderData.geometry.attributes.INDEX - ) - const positions = new Float64Array( - this.renderViews[0].renderData.geometry.attributes.POSITION - ) - const colors = new Float32Array( - this.renderViews[0].renderData.geometry.attributes.COLOR - ) + const indices: number[] | undefined = + this.renderViews[0].renderData.geometry.attributes?.INDEX + + const positions: number[] | undefined = + this.renderViews[0].renderData.geometry.attributes?.POSITION - this.makeInstancedMeshGeometry(indices, positions, colors) + const colors: number[] | undefined = + this.renderViews[0].renderData.geometry.attributes?.COLOR + + /** Catering to typescript + * There is no unniverse where indices or positions are undefined at this point + */ + if (!indices || !positions) { + throw new Error(`Cannot build batch ${this.id}. Undefined indices or positions`) + } + this.makeInstancedMeshGeometry( + positions.length >= 65535 || indices.length >= 65535 + ? new Uint32Array(indices) + : new Uint16Array(indices), + new Float64Array(positions), + colors ? new Float32Array(colors) : undefined + ) this.mesh = new SpeckleInstancedMesh(this.geometry) this.mesh.setBatchObjects(batchObjects) this.mesh.setBatchMaterial(this.batchMaterial) this.mesh.buildTAS() - const bounds = new Box3() - for (let k = 0; k < this.renderViews.length; k++) { - bounds.union(this.renderViews[k].aabb) - } this.geometry.boundingBox = this.mesh.TAS.getBoundingBox(new Box3()) this.geometry.boundingSphere = this.geometry.boundingBox.getBoundingSphere( @@ -564,19 +603,19 @@ export class InstancedMeshBatch implements Batch { ) } - public getRenderView(index: number): NodeRenderView { + public getRenderView(index: number): NodeRenderView | null { index Logger.warn('Deprecated! Use InstancedBatchObject') return null } - public getMaterialAtIndex(index: number): Material { + public getMaterialAtIndex(index: number): Material | null { index Logger.warn('Deprecated! Use InstancedBatchObject') return null } - public getMaterial(rv: NodeRenderView): Material { + public getMaterial(rv: NodeRenderView): Material | null { const group = this.groups.find((value) => { return ( rv.batchStart >= value.start && @@ -629,10 +668,9 @@ export class InstancedMeshBatch implements Batch { data[index] = value } - public purge() { + public purge(): void { this.renderViews.length = 0 this.geometry.dispose() this.batchMaterial.dispose() - this.mesh = null } } diff --git a/packages/viewer/src/modules/batching/LineBatch.ts b/packages/viewer/src/modules/batching/LineBatch.ts index 420ae26834..d911298477 100644 --- a/packages/viewer/src/modules/batching/LineBatch.ts +++ b/packages/viewer/src/modules/batching/LineBatch.ts @@ -1,9 +1,9 @@ import { + Box3, Color, DynamicDrawUsage, InstancedInterleavedBuffer, InterleavedBufferAttribute, - Line, Material, Object3D, Vector4, @@ -16,28 +16,28 @@ import SpeckleLineMaterial from '../materials/SpeckleLineMaterial' import { NodeRenderView } from '../tree/NodeRenderView' import { AllBatchUpdateRange, - Batch, - BatchUpdateRange, + type Batch, + type BatchUpdateRange, + type DrawGroup, GeometryType, NoneBatchUpdateRange } from './Batch' import { ObjectLayers } from '../../IViewer' -import { DrawGroup } from './Batch' import Materials from '../materials/Materials' export default class LineBatch implements Batch { public id: string public subtreeId: string public renderViews: NodeRenderView[] - private geometry: LineSegmentsGeometry + protected geometry: LineSegmentsGeometry public batchMaterial: SpeckleLineMaterial - private mesh: LineSegments2 | Line - public colorBuffer: InstancedInterleavedBuffer + protected mesh: LineSegments2 + public colorBuffer!: InstancedInterleavedBuffer private static readonly vector4Buffer: Vector4 = new Vector4() - public get bounds() { + public get bounds(): Box3 { if (!this.geometry.boundingBox) this.geometry.computeBoundingBox() - return this.geometry.boundingBox + return this.geometry.boundingBox ? this.geometry.boundingBox : new Box3() } public get drawCalls(): number { @@ -65,7 +65,11 @@ export default class LineBatch implements Batch { return 0 } public get lineCount(): number { - return (this.geometry.index.count / 3) * this.geometry['_maxInstanceCount'] + /** Catering to typescript + * There is no unniverse where the geometry is non-indexed. LineSegments2 are **explicitly** indexed + */ + const indexCount = this.geometry.index ? this.geometry.index.count : 0 + return (indexCount / 3) * (this.geometry as never)['_maxInstanceCount'] } public get renderObject(): Object3D { @@ -73,11 +77,11 @@ export default class LineBatch implements Batch { } public get geometryType(): GeometryType { - return this.renderViews[0].geometryType + return GeometryType.LINE } public get materials(): Material[] { - return this.mesh.material as Material[] + return this.mesh.material as unknown as Material[] } public get groups(): DrawGroup[] { @@ -100,7 +104,7 @@ export default class LineBatch implements Batch { renderer.getDrawingBufferSize(this.batchMaterial.resolution) } - public setVisibleRange(...ranges: BatchUpdateRange[]) { + public setVisibleRange(ranges: BatchUpdateRange[]) { if ( ranges.length === 1 && ranges[0].offset === NoneBatchUpdateRange.offset && @@ -163,7 +167,7 @@ export default class LineBatch implements Batch { return NoneBatchUpdateRange } - public setBatchBuffers(...ranges: BatchUpdateRange[]): void { + public setBatchBuffers(ranges: BatchUpdateRange[]): void { const data = this.colorBuffer.array as number[] for (let i = 0; i < ranges.length; i++) { @@ -193,16 +197,18 @@ export default class LineBatch implements Batch { this.geometry.attributes['instanceColorEnd'].needsUpdate = true } - public setDrawRanges(...ranges: BatchUpdateRange[]) { - this.setBatchBuffers(...ranges) + public setDrawRanges(ranges: BatchUpdateRange[]) { + this.setBatchBuffers(ranges) } public resetDrawRanges() { - this.setDrawRanges({ - offset: 0, - count: Infinity, - material: this.batchMaterial - }) + this.setDrawRanges([ + { + offset: 0, + count: Infinity, + material: this.batchMaterial + } + ]) this.mesh.material = this.batchMaterial this.mesh.visible = true this.batchMaterial.transparent = false @@ -210,17 +216,22 @@ export default class LineBatch implements Batch { public buildBatch() { let attributeCount = 0 - this.renderViews.forEach( - (val: NodeRenderView) => - (attributeCount += val.needsSegmentConversion - ? (val.renderData.geometry.attributes.POSITION.length - 3) * 2 - : val.renderData.geometry.attributes.POSITION.length) - ) + this.renderViews.forEach((val: NodeRenderView) => { + if (!val.renderData.geometry.attributes) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry`) + } + attributeCount += val.needsSegmentConversion + ? (val.renderData.geometry.attributes.POSITION.length - 3) * 2 + : val.renderData.geometry.attributes.POSITION.length + }) const position = new Float64Array(attributeCount) let offset = 0 for (let k = 0; k < this.renderViews.length; k++) { const geometry = this.renderViews[k].renderData.geometry - let points = null + if (!geometry.attributes) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry`) + } + let points: Array /** We need to make sure the line geometry has a layout of : * start(x,y,z), end(x,y,z), start(x,y,z), end(x,y,z)... etc * Some geometries have that inherent form, some don't @@ -239,7 +250,7 @@ export default class LineBatch implements Batch { points[2 * i + 5] = geometry.attributes.POSITION[i + 5] } } else { - points = geometry.attributes.POSITION + points = geometry.attributes.POSITION as number[] } position.set(points, offset) @@ -259,7 +270,7 @@ export default class LineBatch implements Batch { this.mesh.layers.set(ObjectLayers.STREAM_CONTENT_LINE) } - public getRenderView(index: number): NodeRenderView { + public getRenderView(index: number): NodeRenderView | null { for (let k = 0; k < this.renderViews.length; k++) { if ( index >= this.renderViews[k].batchStart && @@ -270,6 +281,7 @@ export default class LineBatch implements Batch { return this.renderViews[k] } } + return null } public getMaterialAtIndex(index: number): Material { @@ -336,7 +348,6 @@ export default class LineBatch implements Batch { this.renderViews.length = 0 this.geometry.dispose() this.batchMaterial.dispose() - this.mesh = null this.colorBuffer.length = 0 } } diff --git a/packages/viewer/src/modules/batching/MeshBatch.ts b/packages/viewer/src/modules/batching/MeshBatch.ts index e22ae85188..6d4e847f17 100644 --- a/packages/viewer/src/modules/batching/MeshBatch.ts +++ b/packages/viewer/src/modules/batching/MeshBatch.ts @@ -9,37 +9,33 @@ import { DynamicDrawUsage, Sphere } from 'three' -import { GeometryType, BatchUpdateRange } from './Batch' -import { DrawGroup } from './Batch' import { PrimitiveBatch } from './PrimitiveBatch' import SpeckleMesh, { TransformStorage } from '../objects/SpeckleMesh' import Logger from 'js-logger' import { DrawRanges } from './DrawRanges' import { NodeRenderView } from '../tree/NodeRenderView' +import { type BatchUpdateRange, type DrawGroup, GeometryType } from './Batch' import { BatchObject } from './BatchObject' import { Geometry } from '../converter/Geometry' import { ObjectLayers } from '../../IViewer' export class MeshBatch extends PrimitiveBatch { - protected primitive: SpeckleMesh + protected primitive!: SpeckleMesh protected transformStorage: TransformStorage - private indexBuffer0: BufferAttribute - private indexBuffer1: BufferAttribute + private indexBuffer0!: BufferAttribute + private indexBuffer1!: BufferAttribute private indexBufferIndex = 0 - private drawRanges: DrawRanges = new DrawRanges() + protected drawRanges: DrawRanges = new DrawRanges() get bounds(): Box3 { return this.primitive.TAS.getBoundingBox(new Box3()) } get minDrawCalls(): number { - return [ - ...Array.from( - new Set(this.primitive.geometry.groups.map((value) => value.materialIndex)) - ) - ].length + return [...Array.from(new Set(this.groups.map((value) => value.materialIndex)))] + .length } get triCount(): number { @@ -100,6 +96,9 @@ export class MeshBatch extends PrimitiveBatch { end: number, value: number ): { minIndex: number; maxIndex: number } { + if (!this.primitive.geometry.index) { + throw new Error(`Invalid geometry on batch ${this.id}`) + } const index = this.primitive.geometry.index.array as number[] const data = this.gradientIndexBuffer.array as number[] let minVertexIndex = Infinity @@ -122,7 +121,7 @@ export class MeshBatch extends PrimitiveBatch { } } - public setDrawRanges(...ranges: BatchUpdateRange[]) { + public setDrawRanges(ranges: BatchUpdateRange[]) { // const current = this.groups.slice() // const incoming = ranges.slice() ranges.forEach((value: BatchUpdateRange) => { @@ -130,24 +129,22 @@ export class MeshBatch extends PrimitiveBatch { value.material = this.primitive.getCachedMaterial(value.material) } }) - const materials = ranges.map((val) => { - return val.material + const materials: Array = ranges.map((val: BatchUpdateRange) => { + return val.material as Material }) - const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))] + const uniqueMaterials: Array = [ + ...Array.from(new Set(materials.map((value: Material) => value))) + ] for (let k = 0; k < uniqueMaterials.length; k++) { if (!this.materials.includes(uniqueMaterials[k])) this.materials.push(uniqueMaterials[k]) } - this.primitive.geometry.groups = this.drawRanges.integrateRanges( - this.groups, - this.materials, - ranges - ) + this.groups = this.drawRanges.integrateRanges(this.groups, this.materials, ranges) let count = 0 - this.primitive.geometry.groups.forEach((value) => (count += value.count)) + this.groups.forEach((value) => (count += value.count)) if (count !== this.getCount()) { // Logger.error('Current -> ', current) // Logger.error('Incoming -> ', incoming) @@ -158,7 +155,7 @@ export class MeshBatch extends PrimitiveBatch { }, ${this.getCount()}, ${this.getCount() - count}` ) } - this.setBatchBuffers(...ranges) + this.setBatchBuffers(ranges) this.cleanMaterials() if (this.drawCalls > this.minDrawCalls + 2) { @@ -196,13 +193,22 @@ export class MeshBatch extends PrimitiveBatch { let indicesCount = 0 let attributeCount = 0 for (let k = 0; k < this.renderViews.length; k++) { - indicesCount += this.renderViews[k].renderData.geometry.attributes.INDEX.length - attributeCount += - this.renderViews[k].renderData.geometry.attributes.POSITION.length + const ervee = this.renderViews[k] + /** Catering to typescript + * There is no unniverse where indices or positions are undefined at this point + */ + if ( + !ervee.renderData.geometry.attributes || + !ervee.renderData.geometry.attributes.INDEX + ) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) + } + indicesCount += ervee.renderData.geometry.attributes.INDEX.length + attributeCount += ervee.renderData.geometry.attributes.POSITION.length } const hasVertexColors = - this.renderViews[0].renderData.geometry.attributes.COLOR !== undefined + this.renderViews[0].renderData.geometry.attributes?.COLOR !== undefined const indices = new Uint32Array(indicesCount) const position = new Float64Array(attributeCount) const color = new Float32Array(hasVertexColors ? attributeCount : 0) @@ -215,6 +221,12 @@ export class MeshBatch extends PrimitiveBatch { for (let k = 0; k < this.renderViews.length; k++) { const geometry = this.renderViews[k].renderData.geometry + /** Catering to typescript + * There is no unniverse where indices or positions are undefined at this point + */ + if (!geometry.attributes || !geometry.attributes.INDEX) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) + } indices.set( geometry.attributes.INDEX.map((val) => val + offset / 3), arrayOffset @@ -246,7 +258,7 @@ export class MeshBatch extends PrimitiveBatch { indices, position, batchIndices, - hasVertexColors ? color : null + hasVertexColors ? color : undefined ) this.primitive = new SpeckleMesh(geometry) @@ -310,12 +322,12 @@ export class MeshBatch extends PrimitiveBatch { return geometry } - public getRenderView(index: number): NodeRenderView { + public getRenderView(index: number): NodeRenderView | null { index Logger.warn('Deprecated! Use BatchObject') return null } - public getMaterialAtIndex(index: number): Material { + public getMaterialAtIndex(index: number): Material | null { index Logger.warn('Deprecated! Use BatchObject') return null diff --git a/packages/viewer/src/modules/batching/PointBatch.ts b/packages/viewer/src/modules/batching/PointBatch.ts index ad54faa3f1..60da0ab4eb 100644 --- a/packages/viewer/src/modules/batching/PointBatch.ts +++ b/packages/viewer/src/modules/batching/PointBatch.ts @@ -9,17 +9,16 @@ import { Uint16BufferAttribute, DynamicDrawUsage } from 'three' -import { NodeRenderView } from '../..' -import { GeometryType, BatchUpdateRange } from './Batch' -import { DrawGroup } from './Batch' +import { Geometry } from '../converter/Geometry' +import { NodeRenderView } from '../tree/NodeRenderView' +import { type BatchUpdateRange, type DrawGroup, GeometryType } from './Batch' import { PrimitiveBatch } from './PrimitiveBatch' import { DrawRanges } from './DrawRanges' import Logger from 'js-logger' -import { Geometry } from '../converter/Geometry' import { ObjectLayers } from '../../IViewer' export class PointBatch extends PrimitiveBatch { - protected primitive: Points + protected primitive!: Points protected drawRanges: DrawRanges = new DrawRanges() public get geometryType(): GeometryType { @@ -29,6 +28,8 @@ export class PointBatch extends PrimitiveBatch { if (!this.primitive.geometry.boundingBox) this.primitive.geometry.computeBoundingBox() return this.primitive.geometry.boundingBox + ? this.primitive.geometry.boundingBox + : new Box3() } public get minDrawCalls(): number { @@ -54,9 +55,9 @@ export class PointBatch extends PrimitiveBatch { this.renderViews = renderViews } - public setDrawRanges(...ranges: BatchUpdateRange[]) { - const materials = ranges.map((val) => { - return val.material + public setDrawRanges(ranges: BatchUpdateRange[]) { + const materials: Array = ranges.map((val: BatchUpdateRange) => { + return val.material as Material }) const uniqueMaterials = [...Array.from(new Set(materials.map((value) => value)))] @@ -65,18 +66,14 @@ export class PointBatch extends PrimitiveBatch { this.materials.push(uniqueMaterials[k]) } - this.primitive.geometry.groups = this.drawRanges.integrateRanges( - this.groups, - this.materials, - ranges - ) + this.groups = this.drawRanges.integrateRanges(this.groups, this.materials, ranges) let count = 0 this.groups.forEach((value) => (count += value.count)) if (count !== this.getCount()) { Logger.error(`Draw groups invalid on ${this.id}`) } - this.setBatchBuffers(...ranges) + this.setBatchBuffers(ranges) this.cleanMaterials() if (this.drawCalls > this.minDrawCalls + 2) { @@ -108,10 +105,22 @@ export class PointBatch extends PrimitiveBatch { } protected getCurrentIndexBuffer(): BufferAttribute { + /** Catering to typescript + * There is no unniverse where the geometry is non-indexed. We're **explicitly** setting the index at creation time + */ + if (!this.primitive.geometry.index) { + throw new Error(`Invalid index buffer for batch ${this.id}`) + } return this.primitive.geometry.index } protected getNextIndexBuffer(): BufferAttribute { + /** Catering to typescript + * There is no unniverse where the geometry is non-indexed. We're **explicitly** setting the index at creation time + */ + if (!this.primitive.geometry.index) { + throw new Error(`Invalid index buffer for batch ${this.id}`) + } return new BufferAttribute( (this.primitive.geometry.index.array as Uint16Array | Uint32Array).slice(), this.primitive.geometry.index.itemSize @@ -149,8 +158,14 @@ export class PointBatch extends PrimitiveBatch { public buildBatch(): void { let attributeCount = 0 for (let k = 0; k < this.renderViews.length; k++) { - attributeCount += - this.renderViews[k].renderData.geometry.attributes.POSITION.length + const ervee = this.renderViews[k] + /** Catering to typescript + * There is no unniverse where indices or positions are undefined at this point + */ + if (!ervee.renderData.geometry.attributes) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) + } + attributeCount += ervee.renderData.geometry.attributes.POSITION.length } const position = new Float64Array(attributeCount) const color = new Float32Array(attributeCount).fill(1) @@ -159,11 +174,14 @@ export class PointBatch extends PrimitiveBatch { let indexOffset = 0 for (let k = 0; k < this.renderViews.length; k++) { const geometry = this.renderViews[k].renderData.geometry + if (!geometry.attributes) { + throw new Error(`Cannot build batch ${this.id}. Invalid geometry, or indices`) + } position.set(geometry.attributes.POSITION, offset) if (geometry.attributes.COLOR) color.set(geometry.attributes.COLOR, offset) index.set( new Int32Array(geometry.attributes.POSITION.length / 3).map( - (value, index) => index + indexOffset + (_value, index) => index + indexOffset ), indexOffset ) @@ -218,7 +236,7 @@ export class PointBatch extends PrimitiveBatch { return geometry } - public getRenderView(index: number): NodeRenderView { + public getRenderView(index: number): NodeRenderView | null { for (let k = 0; k < this.renderViews.length; k++) { if ( index >= this.renderViews[k].batchStart && @@ -227,8 +245,9 @@ export class PointBatch extends PrimitiveBatch { return this.renderViews[k] } } + return null } - public getMaterialAtIndex(index: number): Material { + public getMaterialAtIndex(index: number): Material | null { for (let k = 0; k < this.renderViews.length; k++) { if ( index >= this.renderViews[k].batchStart && @@ -248,5 +267,6 @@ export class PointBatch extends PrimitiveBatch { return this.materials[group.materialIndex] } } + return null } } diff --git a/packages/viewer/src/modules/batching/PrimitiveBatch.ts b/packages/viewer/src/modules/batching/PrimitiveBatch.ts index 5250a8ebff..f1ed9088b8 100644 --- a/packages/viewer/src/modules/batching/PrimitiveBatch.ts +++ b/packages/viewer/src/modules/batching/PrimitiveBatch.ts @@ -2,33 +2,32 @@ import { Material, Object3D, BufferGeometry, BufferAttribute, Box3 } from 'three import { NodeRenderView } from '../..' import { AllBatchUpdateRange, - Batch, - BatchUpdateRange, + type Batch, + type BatchUpdateRange, GeometryType, NoneBatchUpdateRange } from './Batch' -import { DrawGroup } from './Batch' +import { type DrawGroup } from './Batch' import Materials from '../materials/Materials' import SpeckleStandardColoredMaterial from '../materials/SpeckleStandardColoredMaterial' -import Logger from 'js-logger' export abstract class Primitive< TGeometry extends BufferGeometry = BufferGeometry, TMaterial extends Material | Material[] = Material | Material[] > extends Object3D { - geometry: TGeometry - material: TMaterial - visible: boolean + geometry!: TGeometry + material!: TMaterial + visible!: boolean } export abstract class PrimitiveBatch implements Batch { - public id: string - public subtreeId: string - public renderViews: NodeRenderView[] - public batchMaterial: Material + public id!: string + public subtreeId!: string + public renderViews!: NodeRenderView[] + public batchMaterial!: Material protected abstract primitive: Primitive - protected gradientIndexBuffer: BufferAttribute + protected gradientIndexBuffer!: BufferAttribute protected needsShuffle: boolean = false abstract get geometryType(): GeometryType @@ -43,7 +42,17 @@ export abstract class PrimitiveBatch implements Batch { } public get groups(): DrawGroup[] { - return this.primitive.geometry.groups + /** We always write to geomtry.groups via the set accessor + * which takes a DrawGroup[], so geometry.groups will always + * be an array of DrawGroup. + * Not to mention that **all our draw groupd are DrawGroup because + * they always have a materialIndex defined** by design and convention!!! + */ + return this.primitive.geometry.groups as DrawGroup[] + } + + public set groups(value: DrawGroup[]) { + this.primitive.geometry.groups = value } public get renderObject(): Object3D { @@ -59,22 +68,21 @@ export abstract class PrimitiveBatch implements Batch { } public getCount(): number { - return this.primitive.geometry.index.count + return this.primitive.geometry.index?.count || 0 } public setBatchMaterial(material: Material): void { this.batchMaterial = material } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public onUpdate(deltaTime: number) { + public onUpdate() { if (this.needsShuffle) { this.shuffleDrawGroups() this.needsShuffle = false } } - public setVisibleRange(...ranges: BatchUpdateRange[]) { + public setVisibleRange(ranges: BatchUpdateRange[]) { /** Entire batch needs to NOT be drawn */ if (ranges.length === 1 && ranges[0] === NoneBatchUpdateRange) { this.primitive.geometry.setDrawRange(0, 0) @@ -97,17 +105,17 @@ export abstract class PrimitiveBatch implements Batch { maxOffset = Math.max(maxOffset, range.offset) }) + const offset = ranges.find((val) => val.offset === maxOffset) this.primitive.geometry.setDrawRange( minOffset, - maxOffset - minOffset + ranges.find((val) => val.offset === maxOffset).count + maxOffset - minOffset + (offset ? offset.count : 0) ) this.primitive.visible = true } public getVisibleRange(): BatchUpdateRange { /** Entire batch is visible */ - if (this.primitive.geometry.groups.length === 1 && this.primitive.visible) - return AllBatchUpdateRange + if (this.groups.length === 1 && this.primitive.visible) return AllBatchUpdateRange /** Entire batch is hidden */ if (!this.primitive.visible) return NoneBatchUpdateRange /** Parts of the batch are visible */ @@ -120,6 +128,7 @@ export abstract class PrimitiveBatch implements Batch { public getOpaque(): BatchUpdateRange { /** If there is any transparent or hidden group return the update range up to it's offset */ const transparentOrHiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return ( Materials.isTransparent(this.materials[value.materialIndex]) || this.materials[value.materialIndex].visible === false @@ -139,6 +148,7 @@ export abstract class PrimitiveBatch implements Batch { public getDepth(): BatchUpdateRange { /** If there is any transparent or hidden group return the update range up to it's offset */ const transparentOrHiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return ( Materials.isTransparent(this.materials[value.materialIndex]) || this.materials[value.materialIndex].visible === false || @@ -159,10 +169,12 @@ export abstract class PrimitiveBatch implements Batch { public getTransparent(): BatchUpdateRange { /** Look for a transparent group */ const transparentGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return Materials.isTransparent(this.materials[value.materialIndex]) }) /** Look for a hidden group */ const hiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].visible === false }) /** If there is a transparent group return it's range */ @@ -185,6 +197,7 @@ export abstract class PrimitiveBatch implements Batch { if (this.materials[0].stencilWrite === true) return AllBatchUpdateRange } const stencilGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false return this.materials[value.materialIndex].stencilWrite === true }) if (stencilGroup) { @@ -197,39 +210,44 @@ export abstract class PrimitiveBatch implements Batch { return NoneBatchUpdateRange } - public setBatchBuffers(...range: BatchUpdateRange[]): void { + public setBatchBuffers(ranges: BatchUpdateRange[]): void { let minGradientIndex = Infinity let maxGradientIndex = 0 - for (let k = 0; k < range.length; k++) { - if (range[k].materialOptions) { - if (range[k].materialOptions.rampIndex !== undefined) { - const start = range[k].offset - const len = range[k].offset + range[k].count + for (let k = 0; k < ranges.length; k++) { + const range = ranges[k] + if (range.materialOptions) { + if ( + range.materialOptions.rampIndex !== undefined && + range.materialOptions.rampWidth !== undefined + ) { + const start = ranges[k].offset + const len = ranges[k].offset + ranges[k].count /** The ramp indices specify the *begining* of each ramp color. When sampling with Nearest filter (since we don't want filtering) * we'll always be sampling right at the edge between texels. Most GPUs will sample consistently, but some won't and we end up with * a ton of artifacts. To avoid this, we are shifting the sampling indices so they're right on the center of each texel, so no inconsistent * sampling can occur. */ - const shiftedIndex = - range[k].materialOptions.rampIndex + - 0.5 / range[k].materialOptions.rampWidth - const minMaxIndices = this.updateGradientIndexBufferData( - start, - range[k].count === Infinity - ? this.primitive.geometry.attributes['gradientIndex'].array.length - : len, - shiftedIndex - ) - minGradientIndex = Math.min(minGradientIndex, minMaxIndices.minIndex) - maxGradientIndex = Math.max(maxGradientIndex, minMaxIndices.maxIndex) + if (range.materialOptions.rampIndex && range.materialOptions.rampWidth) { + const shiftedIndex = + range.materialOptions.rampIndex + 0.5 / range.materialOptions.rampWidth + const minMaxIndices = this.updateGradientIndexBufferData( + start, + range.count === Infinity + ? this.primitive.geometry.attributes['gradientIndex'].array.length + : len, + shiftedIndex + ) + minGradientIndex = Math.min(minGradientIndex, minMaxIndices.minIndex) + maxGradientIndex = Math.max(maxGradientIndex, minMaxIndices.maxIndex) + } } /** We need to update the texture here, because each batch uses it's own clone for any material we use on it * because otherwise three.js won't properly update our custom uniforms */ - if (range[k].materialOptions.rampTexture !== undefined) { - if (range[k].material instanceof SpeckleStandardColoredMaterial) { - ;(range[k].material as SpeckleStandardColoredMaterial).setGradientTexture( - range[k].materialOptions.rampTexture + if (range.materialOptions.rampTexture !== undefined) { + if (range.material instanceof SpeckleStandardColoredMaterial) { + ;(range.material as SpeckleStandardColoredMaterial).setGradientTexture( + range.materialOptions.rampTexture ) } } @@ -242,7 +260,12 @@ export abstract class PrimitiveBatch implements Batch { protected cleanMaterials() { const materialsInUse = [ ...Array.from( - new Set(this.groups.map((value) => this.materials[value.materialIndex])) + new Set( + this.groups.map((value) => { + if (value.materialIndex === undefined) return undefined + return this.materials[value.materialIndex] + }) + ) ) ] let k = 0 @@ -250,6 +273,7 @@ export abstract class PrimitiveBatch implements Batch { if (!materialsInUse.includes(this.materials[k])) { this.materials.splice(k, 1) this.groups.forEach((value: DrawGroup) => { + if (value.materialIndex === undefined) return if (value.materialIndex > k) value.materialIndex-- }) k = 0 @@ -264,13 +288,15 @@ export abstract class PrimitiveBatch implements Batch { protected abstract shuffleMaterialOrder(a: DrawGroup, b: DrawGroup): number private shuffleDrawGroups() { - const groups = this.primitive.geometry.groups.slice() + const groups = this.groups.slice() groups.sort(this.shuffleMaterialOrder.bind(this)) - const materialOrder = [] + const materialOrder: Array = [] groups.reduce((previousValue, currentValue) => { - if (previousValue.indexOf(currentValue.materialIndex) === -1) { - previousValue.push(currentValue.materialIndex) + if (currentValue.materialIndex !== undefined) { + if (previousValue.indexOf(currentValue.materialIndex) === -1) { + previousValue.push(currentValue.materialIndex) + } } return previousValue }, materialOrder) @@ -332,7 +358,7 @@ export abstract class PrimitiveBatch implements Batch { materialIndex: materialGroup[0].materialIndex }) } - this.primitive.geometry.groups = [] + this.groups = [] for (let i = 0; i < newGroups.length; i++) { this.primitive.geometry.addGroup( newGroups[i].offset, @@ -342,16 +368,22 @@ export abstract class PrimitiveBatch implements Batch { } this.primitive.geometry.setIndex(targetIBO) - this.primitive.geometry.index.needsUpdate = true + /** Catering to typescript + * The line above literally makes sure the index is set. Absurd + */ + if (this.primitive.geometry.index) this.primitive.geometry.index.needsUpdate = true - const hiddenGroup = this.primitive.geometry.groups.find((value) => { - return this.primitive.material[value.materialIndex].visible === false + const hiddenGroup = this.groups.find((value) => { + if (value.materialIndex === undefined) return false + return this.materials[value.materialIndex].visible === false }) if (hiddenGroup) { - this.setVisibleRange({ - offset: 0, - count: hiddenGroup.start - }) + this.setVisibleRange([ + { + offset: 0, + count: hiddenGroup.start + } + ]) } // console.log('Final -> ', this.id, this.groups.slice()) } @@ -372,7 +404,7 @@ export abstract class PrimitiveBatch implements Batch { this.primitive.geometry.attributes['gradientIndex'].needsUpdate = true } - public abstract setDrawRanges(...ranges: BatchUpdateRange[]) + public abstract setDrawRanges(ranges: BatchUpdateRange[]): void public resetDrawRanges(): void { this.primitive.visible = true @@ -382,29 +414,21 @@ export abstract class PrimitiveBatch implements Batch { } public abstract buildBatch(): void - public abstract getRenderView(index: number): NodeRenderView - public abstract getMaterialAtIndex(index: number): Material - public getMaterial(rv: NodeRenderView): Material { - for (let k = 0; k < this.primitive.geometry.groups.length; k++) { - try { - if ( - rv.batchStart >= this.primitive.geometry.groups[k].start && - rv.batchEnd <= - this.primitive.geometry.groups[k].start + - this.primitive.geometry.groups[k].count - ) { - return this.materials[this.primitive.geometry.groups[k].materialIndex] - } - } catch (e) { - Logger.error('Failed to get material') + public abstract getRenderView(index: number): NodeRenderView | null + public abstract getMaterialAtIndex(index: number): Material | null + public getMaterial(rv: NodeRenderView): Material | null { + for (let k = 0; k < this.groups.length; k++) { + const group = this.groups[k] + if (rv.batchStart >= group.start && rv.batchEnd <= group.start + group.count) { + return this.materials[group.materialIndex] } } + return null } public purge(): void { this.renderViews.length = 0 this.primitive.geometry.dispose() this.batchMaterial.dispose() - this.primitive = null } } diff --git a/packages/viewer/src/modules/batching/TextBatch.ts b/packages/viewer/src/modules/batching/TextBatch.ts index e330ec58c7..c993632432 100644 --- a/packages/viewer/src/modules/batching/TextBatch.ts +++ b/packages/viewer/src/modules/batching/TextBatch.ts @@ -4,23 +4,23 @@ import { Box3, Material, Object3D, WebGLRenderer } from 'three' import { NodeRenderView } from '../tree/NodeRenderView' import { AllBatchUpdateRange, - Batch, - BatchUpdateRange, + type Batch, + type BatchUpdateRange, + type DrawGroup, GeometryType, NoneBatchUpdateRange } from './Batch' import { SpeckleText } from '../objects/SpeckleText' import { ObjectLayers } from '../../IViewer' -import { DrawGroup } from './Batch' import Materials from '../materials/Materials' export default class TextBatch implements Batch { public id: string public subtreeId: string public renderViews: NodeRenderView[] - public batchMaterial: Material - public mesh: SpeckleText + public batchMaterial!: Material + public mesh!: SpeckleText public get bounds(): Box3 { return new Box3().setFromObject(this.mesh) @@ -68,7 +68,7 @@ export default class TextBatch implements Batch { public getCount(): number { return ( this.mesh.textMesh.geometry.index.count + - this.mesh.backgroundMesh?.geometry.index.count + this.mesh.backgroundMesh?.geometry.index?.count ) } @@ -92,7 +92,8 @@ export default class TextBatch implements Batch { renderer } - public setVisibleRange(...ranges: BatchUpdateRange[]) { + public setVisibleRange(ranges: BatchUpdateRange[]) { + ranges // TO DO } @@ -116,11 +117,12 @@ export default class TextBatch implements Batch { return NoneBatchUpdateRange } - public setBatchBuffers(...range: BatchUpdateRange[]): void { + public setBatchBuffers(range: BatchUpdateRange[]): void { + range throw new Error('Method not implemented.') } - public setDrawRanges(...ranges: BatchUpdateRange[]) { + public setDrawRanges(ranges: BatchUpdateRange[]) { this.mesh.textMesh.material = ranges[0].material if (ranges[0].materialOptions && ranges[0].materialOptions.rampIndexColor) { this.mesh.textMesh.material.color.copy(ranges[0].materialOptions.rampIndexColor) @@ -130,11 +132,15 @@ export default class TextBatch implements Batch { public resetDrawRanges() { this.mesh.textMesh.material = this.batchMaterial this.mesh.textMesh.visible = true - // this.geometry.clearGroups() - // this.geometry.setDrawRange(0, Infinity) } public async buildBatch() { + /** Catering to typescript + * There is no unniverse where there is no metadata + */ + if (!this.renderViews[0].renderData.geometry.metaData) { + throw new Error(`Cannot build batch ${this.id}. Metadata`) + } this.mesh = new SpeckleText(this.id, ObjectLayers.STREAM_CONTENT_TEXT) this.mesh.matrixAutoUpdate = false await this.mesh.update( @@ -142,7 +148,8 @@ export default class TextBatch implements Batch { this.renderViews[0].renderData.geometry.metaData ) ) - this.mesh.matrix.copy(this.renderViews[0].renderData.geometry.bakeTransform) + if (this.renderViews[0].renderData.geometry.bakeTransform) + this.mesh.matrix.copy(this.renderViews[0].renderData.geometry.bakeTransform) this.renderViews[0].setBatchData( this.id, 0, @@ -152,14 +159,17 @@ export default class TextBatch implements Batch { } public getRenderView(index: number): NodeRenderView { + index return this.renderViews[0] } public getMaterialAtIndex(index: number): Material { + index return this.batchMaterial } public getMaterial(rv: NodeRenderView): Material { + rv return this.batchMaterial } @@ -167,6 +177,5 @@ export default class TextBatch implements Batch { this.renderViews.length = 0 this.batchMaterial.dispose() this.mesh.geometry.dispose() - this.mesh = null } } diff --git a/packages/viewer/src/modules/converter/Geometry.ts b/packages/viewer/src/modules/converter/Geometry.ts index 907a8d219f..83ebd50f94 100644 --- a/packages/viewer/src/modules/converter/Geometry.ts +++ b/packages/viewer/src/modules/converter/Geometry.ts @@ -8,7 +8,7 @@ import { Matrix4, Vector3 } from 'three' -import { SpeckleObject } from '../tree/DataTree' +import { type SpeckleObject } from '../../IViewer' export enum GeometryAttributes { POSITION = 'POSITION', @@ -20,9 +20,12 @@ export enum GeometryAttributes { } export interface GeometryData { - attributes: Partial> - bakeTransform: Matrix4 - transform: Matrix4 + attributes: + | (Record & + Partial>) + | null + bakeTransform: Matrix4 | null + transform: Matrix4 | null metaData?: SpeckleObject instanced?: boolean } @@ -74,26 +77,33 @@ export class Geometry { } static mergeGeometryAttribute( - attributes: number[][], + attributes: (number[] | undefined)[], target: Float32Array | Float64Array ): ArrayLike { let offset = 0 for (let k = 0; k < attributes.length; k++) { - target.set(attributes[k], offset) - offset += attributes[k].length + const attribute = attributes[k] + if (!attribute || !target) { + throw new Error('Cannot merge geometries. Indices or positions are undefined') + } + target.set(attribute, offset) + offset += attribute.length } return target } static mergeIndexAttribute( - indexAttributes: number[][], - positionAttributes: number[][] + indexAttributes: (number[] | undefined)[], + positionAttributes: (number[] | undefined)[] ): number[] { let indexOffset = 0 const mergedIndex = [] for (let i = 0; i < indexAttributes.length; ++i) { const index = indexAttributes[i] + if (!index || !positionAttributes) { + throw new Error('Cannot merge geometries. Indices or positions are undefined') + } for (let j = 0; j < index.length; ++j) { mergedIndex.push(index[j] + indexOffset) @@ -113,46 +123,74 @@ export class Geometry { } as GeometryData for (let i = 0; i < geometries.length; i++) { - if (geometries[i].bakeTransform) + /** Catering to typescript */ + if (geometries[i].bakeTransform !== null) Geometry.transformGeometryData(geometries[i], geometries[i].bakeTransform) } - if (sampleAttributes[GeometryAttributes.INDEX]) { - const indexAttributes = geometries.map( - (item) => item.attributes[GeometryAttributes.INDEX] - ) - const positionAttributes = geometries.map( - (item) => item.attributes[GeometryAttributes.POSITION] + if (sampleAttributes && sampleAttributes[GeometryAttributes.INDEX]) { + const indexAttributes: (number[] | undefined)[] = geometries.map( + (item: GeometryData) => { + /** Catering to typescript */ + if (!item.attributes) return + return item.attributes[GeometryAttributes.INDEX] + } ) - mergedGeometry.attributes[GeometryAttributes.INDEX] = - Geometry.mergeIndexAttribute(indexAttributes, positionAttributes) + const positionAttributes: (number[] | undefined)[] = geometries.map((item) => { + /** Catering to typescript */ + if (!item.attributes) return + return item.attributes[GeometryAttributes.POSITION] + }) + /** o_0 Catering to typescript*/ + if (mergedGeometry.attributes) + mergedGeometry.attributes[GeometryAttributes.INDEX] = + Geometry.mergeIndexAttribute(indexAttributes, positionAttributes) } for (const k in sampleAttributes) { if (k !== GeometryAttributes.INDEX) { - const attributes = geometries.map((item) => { - return item.attributes[k] + const attributes: (number[] | undefined)[] = geometries.map((item) => { + /** Catering to typescript */ + if (!item.attributes) return + return item.attributes[k as GeometryAttributes] as number[] }) - mergedGeometry.attributes[k] = Geometry.mergeGeometryAttribute( - attributes, - k === GeometryAttributes.POSITION - ? new Float64Array(attributes.reduce((prev, cur) => prev + cur.length, 0)) - : new Float32Array(attributes.reduce((prev, cur) => prev + cur.length, 0)) - ) + /** Catering to typescript */ + if (mergedGeometry.attributes) + mergedGeometry.attributes[k as GeometryAttributes] = + Geometry.mergeGeometryAttribute( + attributes, + k === GeometryAttributes.POSITION + ? new Float64Array( + attributes.reduce((prev, cur) => { + /** Catering to typescript */ + if (!cur) return 0 + return prev + cur.length + }, 0) + ) + : new Float32Array( + attributes.reduce((prev, cur) => { + /** Catering to typescript */ + if (!cur) return 0 + return prev + cur.length + }, 0) + ) + ) as number[] } } geometries.forEach((geometry) => { for (const k in geometry.attributes) { - delete geometry.attributes[k] + delete geometry.attributes[k as GeometryAttributes] } }) return mergedGeometry } - public static transformGeometryData(geometryData: GeometryData, m: Matrix4) { + public static transformGeometryData(geometryData: GeometryData, m: Matrix4 | null) { + if (!geometryData.attributes) return if (!geometryData.attributes.POSITION) return + if (!m) return const e = m.elements diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 0fb417d672..ddd3ed1a23 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -4,10 +4,10 @@ import { Extension } from './Extension' import { SpeckleCameraControls } from '../objects/SpeckleCameraControls' import { Box3, OrthographicCamera, PerspectiveCamera, Sphere, Vector3 } from 'three' import { KeyboardKeyHold, HOLD_EVENT_TYPE } from 'hold-event' -import { CameraProjection } from '../objects/SpeckleCamera' -import { CameraEvent, SpeckleCamera } from '../objects/SpeckleCamera' +import { CameraProjection, type CameraEventPayload } from '../objects/SpeckleCamera' +import { CameraEvent, type SpeckleCamera } from '../objects/SpeckleCamera' import Logger from 'js-logger' -import { IViewer, SpeckleView } from '../../IViewer' +import type { IViewer, SpeckleView } from '../../IViewer' export type CanonicalView = | 'front' @@ -33,10 +33,10 @@ export type PolarView = { } export class CameraController extends Extension implements SpeckleCamera { - protected _renderingCamera: PerspectiveCamera | OrthographicCamera = null - protected perspectiveCamera: PerspectiveCamera = null - protected orthographicCamera: OrthographicCamera = null - protected _controls: SpeckleCameraControls = null + protected _renderingCamera!: PerspectiveCamera | OrthographicCamera + protected perspectiveCamera: PerspectiveCamera + protected orthographicCamera: OrthographicCamera + protected _controls: SpeckleCameraControls get renderingCamera(): PerspectiveCamera | OrthographicCamera { return this._renderingCamera @@ -46,15 +46,15 @@ export class CameraController extends Extension implements SpeckleCamera { this._renderingCamera = value } - public get enabled() { + public get enabled(): boolean { return this._controls.enabled } - public set enabled(val) { + public set enabled(val: boolean) { this._controls.enabled = val } - public get fieldOfView() { + public get fieldOfView(): number { return this.perspectiveCamera.fov } @@ -63,11 +63,11 @@ export class CameraController extends Extension implements SpeckleCamera { this.perspectiveCamera.updateProjectionMatrix() } - public get aspect() { + public get aspect(): number { return this.perspectiveCamera.aspect } - public get controls() { + public get controls(): SpeckleCameraControls { return this._controls } @@ -131,14 +131,33 @@ export class CameraController extends Extension implements SpeckleCamera { this.viewer.getRenderer().speckleCamera = this } - setCameraView(objectIds: string[], transition: boolean, fit?: number): void + public on( + eventType: T, + listener: (arg: CameraEventPayload[T]) => void + ): void { + super.on(eventType, listener) + } + + setCameraView( + objectIds: string[] | undefined, + transition: boolean | undefined, + fit?: number + ): void setCameraView( view: CanonicalView | SpeckleView | InlineView | PolarView, - transition: boolean + transition: boolean | undefined, + fit?: number ): void - setCameraView(bounds: Box3, transition: boolean): void + setCameraView(bounds: Box3, transition: boolean | undefined, fit?: number): void setCameraView( - arg0: string[] | CanonicalView | SpeckleView | InlineView | PolarView | Box3, + arg0: + | string[] + | CanonicalView + | SpeckleView + | InlineView + | PolarView + | Box3 + | undefined, arg1 = true, arg2 = 1.2 ): void { @@ -194,19 +213,19 @@ export class CameraController extends Extension implements SpeckleCamera { this.viewer.requestRender() } - public setOrthoCameraOn() { + public setOrthoCameraOn(): void { if (this._renderingCamera === this.orthographicCamera) return this.renderingCamera = this.orthographicCamera this.setupOrthoCamera() this.viewer.requestRender() } - public toggleCameras() { + public toggleCameras(): void { if (this._renderingCamera === this.perspectiveCamera) this.setOrthoCameraOn() else this.setPerspectiveCameraOn() } - protected setupOrthoCamera() { + protected setupOrthoCamera(): void { this._controls.mouseButtons.wheel = CameraControls.ACTION.ZOOM const lineOfSight = new Vector3() @@ -252,11 +271,11 @@ export class CameraController extends Extension implements SpeckleCamera { this.emit(CameraEvent.ProjectionChanged, CameraProjection.PERSPECTIVE) } - public disableRotations() { + public disableRotations(): void { this._controls.mouseButtons.left = CameraControls.ACTION.TRUCK } - public enableRotations() { + public enableRotations(): void { this._controls.mouseButtons.left = CameraControls.ACTION.ROTATE } @@ -269,7 +288,7 @@ export class CameraController extends Extension implements SpeckleCamera { const dKey = new KeyboardKeyHold(KEYCODE.D, 16.666) const isTruckingGroup = new Array(4) - const setTrucking = (index, value) => { + const setTrucking = (index: number, value: boolean) => { isTruckingGroup[index] = value if (isTruckingGroup.every((element) => element === false)) { this._controls.isTrucking = false @@ -277,21 +296,15 @@ export class CameraController extends Extension implements SpeckleCamera { } else this._controls.isTrucking = true } - aKey.addEventListener( - HOLD_EVENT_TYPE.HOLD_START, - function () { - this.controls.dispatchEvent({ type: 'controlstart' }) - }.bind(this) - ) - aKey.addEventListener( - 'holding', - function (event) { - if (this.viewer.mouseOverRenderer === false) return - setTrucking(0, true) - this.controls.truck(-0.01 * event.deltaTime, 0, false) - return - }.bind(this) - ) + aKey.addEventListener(HOLD_EVENT_TYPE.HOLD_START, () => { + this.controls['dispatchEvent']({ type: 'controlstart' }) + }) + aKey.addEventListener('holding', (event) => { + if (!event) return + setTrucking(0, true) + this.controls.truck(-0.01 * event.deltaTime, 0, false) + return + }) aKey.addEventListener( HOLD_EVENT_TYPE.HOLD_END, function () { @@ -299,21 +312,15 @@ export class CameraController extends Extension implements SpeckleCamera { }.bind(this) ) - dKey.addEventListener( - HOLD_EVENT_TYPE.HOLD_START, - function () { - this.controls.dispatchEvent({ type: 'controlstart' }) - }.bind(this) - ) - dKey.addEventListener( - 'holding', - function (event) { - if (this.viewer.mouseOverRenderer === false) return - setTrucking(1, true) - this.controls.truck(0.01 * event.deltaTime, 0, false) - return - }.bind(this) - ) + dKey.addEventListener(HOLD_EVENT_TYPE.HOLD_START, () => { + this.controls['dispatchEvent']({ type: 'controlstart' }) + }) + dKey.addEventListener('holding', (event) => { + if (!event) return + setTrucking(1, true) + this.controls.truck(0.01 * event.deltaTime, 0, false) + return + }) dKey.addEventListener( HOLD_EVENT_TYPE.HOLD_END, function () { @@ -321,21 +328,15 @@ export class CameraController extends Extension implements SpeckleCamera { }.bind(this) ) - wKey.addEventListener( - HOLD_EVENT_TYPE.HOLD_START, - function () { - this.controls.dispatchEvent({ type: 'controlstart' }) - }.bind(this) - ) - wKey.addEventListener( - 'holding', - function (event) { - if (this.viewer.mouseOverRenderer === false) return - setTrucking(2, true) - this.controls.forward(0.01 * event.deltaTime, false) - return - }.bind(this) - ) + wKey.addEventListener(HOLD_EVENT_TYPE.HOLD_START, () => { + this.controls['dispatchEvent']({ type: 'controlstart' }) + }) + wKey.addEventListener('holding', (event) => { + if (!event) return + setTrucking(2, true) + this.controls.forward(0.01 * event.deltaTime, false) + return + }) wKey.addEventListener( HOLD_EVENT_TYPE.HOLD_END, function () { @@ -343,21 +344,15 @@ export class CameraController extends Extension implements SpeckleCamera { }.bind(this) ) - sKey.addEventListener( - HOLD_EVENT_TYPE.HOLD_START, - function () { - this.controls.dispatchEvent({ type: 'controlstart' }) - }.bind(this) - ) - sKey.addEventListener( - 'holding', - function (event) { - if (this.viewer.mouseOverRenderer === false) return - setTrucking(3, true) - this.controls.forward(-0.01 * event.deltaTime, false) - return - }.bind(this) - ) + sKey.addEventListener(HOLD_EVENT_TYPE.HOLD_START, () => { + this.controls['dispatchEvent']({ type: 'controlstart' }) + }) + sKey.addEventListener('holding', (event) => { + if (!event) return + setTrucking(3, true) + this.controls.forward(-0.01 * event.deltaTime, false) + return + }) sKey.addEventListener( HOLD_EVENT_TYPE.HOLD_END, function () { @@ -418,7 +413,7 @@ export class CameraController extends Extension implements SpeckleCamera { // this.viewer.controls.setBoundary( box ) } - private zoomToBox(box, fit = 1.2, transition = true) { + private zoomToBox(box: Box3, fit = 1.2, transition = true) { if (box.max.x === Infinity || box.max.x === -Infinity) { box = new Box3(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)) } @@ -472,8 +467,10 @@ export class CameraController extends Extension implements SpeckleCamera { ) } - private isBox3(view: unknown): view is Box3 { - return view['isBox3'] + private isBox3( + view: CanonicalView | SpeckleView | InlineView | PolarView | Box3 + ): view is Box3 { + return view instanceof Box3 } protected setView( @@ -496,12 +493,12 @@ export class CameraController extends Extension implements SpeckleCamera { private setViewSpeckle(view: SpeckleView, transition = true) { this._controls.setLookAt( - view.view.origin['x'], - view.view.origin['y'], - view.view.origin['z'], - view.view.target['x'], - view.view.target['y'], - view.view.target['z'], + view.view.origin.x, + view.view.origin.y, + view.view.origin.z, + view.view.target.x, + view.view.target.y, + view.view.target.z, transition ) this.enableRotations() diff --git a/packages/viewer/src/modules/extensions/DiffExtension.ts b/packages/viewer/src/modules/extensions/DiffExtension.ts index 46c3b26f07..84d0f8bb2d 100644 --- a/packages/viewer/src/modules/extensions/DiffExtension.ts +++ b/packages/viewer/src/modules/extensions/DiffExtension.ts @@ -1,20 +1,18 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Color, DoubleSide, FrontSide } from 'three' -import { TreeNode, WorldTree } from '../tree/WorldTree' +import { Color, DoubleSide, FrontSide, Material } from 'three' +import { type TreeNode, WorldTree } from '../tree/WorldTree' import Logger from 'js-logger' -import _, { omit } from 'underscore' +import { groupBy } from 'lodash-es' import { GeometryType } from '../batching/Batch' import SpeckleLineMaterial from '../materials/SpeckleLineMaterial' import SpecklePointMaterial from '../materials/SpecklePointMaterial' import SpeckleStandardMaterial from '../materials/SpeckleStandardMaterial' import { NodeRenderView } from '../tree/NodeRenderView' -import { IViewer } from '../../IViewer' +import { type IViewer } from '../../IViewer' import { Extension } from './Extension' import { SpeckleTypeAllRenderables } from '../loaders/GeometryConverter' import { SpeckleLoader } from '../loaders/Speckle/SpeckleLoader' -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type SpeckleObject = Record type SpeckleMaterialType = | SpeckleStandardMaterial | SpecklePointMaterial @@ -26,10 +24,10 @@ export enum VisualDiffMode { } export interface DiffResult { - unchanged: Array - added: Array - removed: Array - modified: Array> + unchanged: Array + added: Array + removed: Array + modified: Array> } interface VisualDiffResult { @@ -49,24 +47,30 @@ export class DiffExtension extends Extension { this._enabled = value } - protected tree: WorldTree = null - private addedMaterialMesh: SpeckleStandardMaterial = null - private changedNewMaterialMesh: SpeckleStandardMaterial = null - private changedOldMaterialMesh: SpeckleStandardMaterial = null - private removedMaterialMesh: SpeckleStandardMaterial = null + protected tree: WorldTree + private addedMaterialMesh: SpeckleStandardMaterial + private changedNewMaterialMesh: SpeckleStandardMaterial + private changedOldMaterialMesh: SpeckleStandardMaterial + private removedMaterialMesh: SpeckleStandardMaterial - private addedMaterialPoint: SpecklePointMaterial = null - private changedNewMaterialPoint: SpecklePointMaterial = null - private changedOldMaterialPoint: SpecklePointMaterial = null - private removedMaterialPoint: SpecklePointMaterial = null + private addedMaterialPoint: SpecklePointMaterial + private changedNewMaterialPoint: SpecklePointMaterial + private changedOldMaterialPoint: SpecklePointMaterial + private removedMaterialPoint: SpecklePointMaterial private addedMaterials: Array = [] private changedOldMaterials: Array = [] private changedNewMaterials: Array = [] private removedMaterials: Array = [] - private _materialGroups = null - private _visualDiff: VisualDiffResult = null + private _materialGroups: + | { + rvs: NodeRenderView[] + material: SpeckleMaterialType + }[] + | null + + private _visualDiff!: VisualDiffResult private _diffTime = -1 private _diffMode: VisualDiffMode = VisualDiffMode.COLORED @@ -237,7 +241,7 @@ export class DiffExtension extends Extension { } /** Currently, the diff does not store the existing materials. We can do that if we need to */ - public async undiff() { + public async undiff(): Promise { const pipelineOptions = this.viewer.getRenderer().pipelineOptions pipelineOptions.depthSide = DoubleSide this.viewer.getRenderer().pipelineOptions = pipelineOptions @@ -253,12 +257,6 @@ export class DiffExtension extends Extension { await Promise.all(unloadPromises) } - private intersection(o1, o2) { - const [k1, k2] = [Object.keys(o1), Object.keys(o2)] - const [first, next] = k1.length > k2.length ? [k2, o1] : [k1, o2] - return first.filter((k) => k in next) - } - private buildIdMaps( rvs: Array, idMap: { [id: string]: { node: TreeNode; applicationId: string } }, @@ -288,93 +286,7 @@ export class DiffExtension extends Extension { return Promise.resolve(diffResult) } - private diffBoolean(urlA: string, urlB: string): Promise { - const start = performance.now() - const diffResult: DiffResult = { - unchanged: [], - added: [], - removed: [], - modified: [] - } - - const renderTreeA = this.tree.getRenderTree(urlA) - const renderTreeB = this.tree.getRenderTree(urlB) - let rvsA = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) - let rvsB = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) - - rvsA = rvsA.map((value) => { - return renderTreeA.getAtomicParent(value) - }) - - rvsB = rvsB.map((value) => { - return renderTreeB.getAtomicParent(value) - }) - - rvsA = [...Array.from(new Set(rvsA))] - rvsB = [...Array.from(new Set(rvsB))] - - const idMapA = {} - const appIdMapA = {} - this.buildIdMaps(rvsA, idMapA, appIdMapA) - - const idMapB = {} - const appIdMapB = {} - this.buildIdMaps(rvsB, idMapB, appIdMapB) - - /** Get the ids which are common between the two maps. This will be objects - * which have not changed - */ - const unchanged: Array = this.intersection(idMapA, idMapB) - /** We remove the unchanged objects from B and end up with changed + added */ - const addedModified = _.omit(idMapB, unchanged) - /** We remove the unchanged objects from A and end up with changed + removed */ - const removedModified = _.omit(idMapA, unchanged) - /** We remove the changed objects from B. An object from B is changed if - * it's application ID exists in A - */ - const added = _.omit(addedModified, function (value, key, object) { - return value.applicationId && appIdMapA[value.applicationId] !== undefined - }) - /** We remove the changed objects from A. An object from A is changed if - * it's application ID exists in B - */ - const removed = _.omit(removedModified, function (value, key, object) { - return value.applicationId && appIdMapB[value.applicationId] !== undefined - }) - /** We remove the removed objects from A, leaving us only changed objects */ - const modifiedRemoved = _.omit(removedModified, Object.keys(removed)) - /** We remove the removed objects from B, leaving us only changed objects */ - const modifiedAdded = _.omit(addedModified, Object.keys(added)) - - /** We fill the arrays from here on out */ - const modifiedOld = Object.values(modifiedRemoved).map( - (value: { node: TreeNode }) => value.node - ) - const modifiedNew = Object.values(modifiedAdded).map( - (value: { node: TreeNode }) => value.node - ) - diffResult.unchanged.push(...unchanged.map((value) => idMapA[value].node)) - diffResult.unchanged.push(...unchanged.map((value) => idMapB[value].node)) - diffResult.removed.push( - ...Object.values(removed).map((value: { node: TreeNode }) => value.node) - ) - diffResult.added.push( - ...Object.values(added).map((value: { node: TreeNode }) => value.node) - ) - - modifiedOld.forEach((value, index) => { - value - diffResult.modified.push([modifiedOld[index], modifiedNew[index]]) - }) - console.warn('Boolean Time -> ', performance.now() - start) - return Promise.resolve(diffResult) - } - private diffIterative(urlA: string, urlB: string): Promise { - const start = performance.now() - const modifiedNew: Array = [] - const modifiedOld: Array = [] - const diffResult: DiffResult = { unchanged: [], added: [], @@ -384,8 +296,18 @@ export class DiffExtension extends Extension { const renderTreeA = this.tree.getRenderTree(urlA) const renderTreeB = this.tree.getRenderTree(urlB) - let rvsA = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) - let rvsB = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) + if (!renderTreeA) { + return Promise.reject( + `Could not make diff. Resource ${urlA} could not be fetched` + ) + } + if (!renderTreeB) { + return Promise.reject( + `Could not make diff. Resource ${urlB} could not be fetched` + ) + } + let rvsA: TreeNode[] = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) + let rvsB: TreeNode[] = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) rvsA = rvsA.map((value) => { return renderTreeA.getAtomicParent(value) @@ -398,12 +320,12 @@ export class DiffExtension extends Extension { rvsA = [...Array.from(new Set(rvsA))] rvsB = [...Array.from(new Set(rvsB))] - const idMapA = {} - const appIdMapA = {} + const idMapA: { [id: string]: { node: TreeNode; applicationId: string } } = {} + const appIdMapA: { [id: string]: TreeNode } = {} this.buildIdMaps(rvsA, idMapA, appIdMapA) - const idMapB = {} - const appIdMapB = {} + const idMapB: { [id: string]: { node: TreeNode; applicationId: string } } = {} + const appIdMapB: { [id: string]: TreeNode } = {} this.buildIdMaps(rvsB, idMapB, appIdMapB) for (let k = 0; k < rvsB.length; k++) { @@ -448,28 +370,27 @@ export class DiffExtension extends Extension { } } - console.warn('Interative Time -> ', performance.now() - start) - return Promise.resolve(diffResult) } - public updateVisualDiff(time?: number, mode?: VisualDiffMode) { - if ( - (mode !== undefined && mode !== this._diffMode) || - this._materialGroups === null - ) { + public updateVisualDiff(time?: number, mode?: VisualDiffMode): void { + if ((mode !== undefined && mode !== this._diffMode) || !this._materialGroups) { this.resetMaterialGroups() - this.buildMaterialGroups(mode) - this._diffMode = mode + /** Catering to typescript */ + if (mode !== undefined) { + this.buildMaterialGroups(mode) + this._diffMode = mode + } } if (time !== undefined && time !== this._diffTime) { this.setDiffTime(time) this._diffTime = time } - this._materialGroups.forEach((value) => { - this.viewer.getRenderer().setMaterial(value.rvs, value.material) - }) + if (this._materialGroups) + this._materialGroups.forEach((value) => { + this.viewer.getRenderer().setMaterial(value.rvs, value.material) + }) this.viewer.requestRender() } @@ -479,7 +400,9 @@ export class DiffExtension extends Extension { this.addedMaterials.forEach((mat) => { mat.opacity = - mat['clampOpacity'] !== undefined ? Math.min(from, mat['clampOpacity']) : from + (mat as never)['clampOpacity'] !== undefined + ? Math.min(from, (mat as never)['clampOpacity']) + : from mat.depthWrite = from < 0.5 ? false : true mat.transparent = mat.opacity < 1 mat.needsCopy = true @@ -487,7 +410,9 @@ export class DiffExtension extends Extension { this.changedOldMaterials.forEach((mat) => { mat.opacity = - mat['clampOpacity'] !== undefined ? Math.min(to, mat['clampOpacity']) : to + (mat as never)['clampOpacity'] !== undefined + ? Math.min(to, (mat as never)['clampOpacity']) + : to mat.depthWrite = to < 0.5 ? false : true mat.transparent = mat.opacity < 1 mat.needsCopy = true @@ -495,7 +420,9 @@ export class DiffExtension extends Extension { this.changedNewMaterials.forEach((mat) => { mat.opacity = - mat['clampOpacity'] !== undefined ? Math.min(from, mat['clampOpacity']) : from + (mat as never)['clampOpacity'] !== undefined + ? Math.min(from, (mat as never)['clampOpacity']) + : from mat.depthWrite = from < 0.5 ? false : true mat.transparent = mat.opacity < 1 mat.needsCopy = true @@ -503,7 +430,9 @@ export class DiffExtension extends Extension { this.removedMaterials.forEach((mat) => { mat.opacity = - mat['clampOpacity'] !== undefined ? Math.min(to, mat['clampOpacity']) : to + (mat as never)['clampOpacity'] !== undefined + ? Math.min(to, (mat as never)['clampOpacity']) + : to mat.depthWrite = to < 0.5 ? false : true mat.transparent = mat.opacity < 1 mat.needsCopy = true @@ -535,31 +464,25 @@ export class DiffExtension extends Extension { const renderTree = this.tree.getRenderTree() const addedRvs = diffResult.added.flatMap((value) => { - return renderTree.getRenderViewsForNode(value as TreeNode, value as TreeNode) + return renderTree.getRenderViewsForNode(value as TreeNode) }) const removedRvs = diffResult.removed.flatMap((value) => { - return renderTree.getRenderViewsForNode(value as TreeNode, value as TreeNode) + return renderTree.getRenderViewsForNode(value as TreeNode) }) const unchangedRvs = diffResult.unchanged.flatMap((value) => { - return renderTree.getRenderViewsForNode(value as TreeNode, value as TreeNode) + return renderTree.getRenderViewsForNode(value as TreeNode) }) const modifiedOldRvs = diffResult.modified .flatMap((value) => { - return renderTree.getRenderViewsForNode( - value[0] as TreeNode, - value[0] as TreeNode - ) + return renderTree.getRenderViewsForNode(value[0] as TreeNode) }) .filter((value) => { return !unchangedRvs.includes(value) && !removedRvs.includes(value) }) const modifiedNewRvs = diffResult.modified .flatMap((value) => { - return renderTree.getRenderViewsForNode( - value[1] as TreeNode, - value[1] as TreeNode - ) + return renderTree.getRenderViewsForNode(value[1] as TreeNode) }) .filter((value) => { return !unchangedRvs.includes(value) && !addedRvs.includes(value) @@ -665,23 +588,42 @@ export class DiffExtension extends Extension { const changedOld = this.getBatchesSubgroups(visualDiffResult.modifiedOld) const changedNew = this.getBatchesSubgroups(visualDiffResult.modifiedNew) const removed = this.getBatchesSubgroups(visualDiffResult.removed) - this.addedMaterials = added.map((value) => value.material) - this.changedOldMaterials = changedOld.map((value) => value.material) - this.changedNewMaterials = changedNew.map((value) => value.material) - this.removedMaterials = removed.map((value) => value.material) + this.addedMaterials = added.map( + (value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) => + value.material + ) + this.changedOldMaterials = changedOld.map( + (value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) => + value.material + ) + this.changedNewMaterials = changedNew.map( + (value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) => + value.material + ) + this.removedMaterials = removed.map( + (value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) => + value.material + ) return [...added, ...changedOld, ...changedNew, ...removed] } - private getBatchesSubgroups(subgroup: Array) { - const groupBatches = _.groupBy(subgroup, 'batchId') + private getBatchesSubgroups(subgroup: Array): { + rvs: NodeRenderView[] + material: SpeckleMaterialType + }[] { + const groupBatches = groupBy(subgroup, 'batchId') - const materialGroup = [] + const materialGroup: { + rvs: NodeRenderView[] + material: SpeckleMaterialType + }[] = [] for (const k in groupBatches) { - const matClone = this.viewer - .getRenderer() - .getBatchMaterial(groupBatches[k][0]) - .clone() - matClone['clampOpacity'] = matClone.opacity + const matClone: SpeckleMaterialType = ( + this.viewer.getRenderer().getBatchMaterial(groupBatches[k][0]) as Material + ).clone() as SpeckleMaterialType + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(matClone as any)['clampOpacity'] = matClone.opacity matClone.opacity = 0.5 matClone.transparent = true materialGroup.push({ @@ -692,4 +634,114 @@ export class DiffExtension extends Extension { return materialGroup } + + /** Keeping this for reference */ + // private intersection(o1: object, o2: object) { + // const [k1, k2] = [Object.keys(o1), Object.keys(o2)] + // const [first, next] = k1.length > k2.length ? [k2, o1] : [k1, o2] + // return first.filter((k) => k in next) + // } + + // private diffBoolean(urlA: string, urlB: string): Promise { + // const diffResult: DiffResult = { + // unchanged: [], + // added: [], + // removed: [], + // modified: [] + // } + + // const renderTreeA = this.tree!.getRenderTree(urlA) + // const renderTreeB = this.tree!.getRenderTree(urlB) + // if (!renderTreeA) { + // return Promise.reject( + // `Could not make diff. Resource ${urlA} could not be fetched` + // ) + // } + // if (!renderTreeB) { + // return Promise.reject( + // `Could not make diff. Resource ${urlB} could not be fetched` + // ) + // } + // let rvsA: TreeNode[] = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables) + // let rvsB: TreeNode[] = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables) + + // rvsA = rvsA.map((value: TreeNode) => { + // return renderTreeA.getAtomicParent(value) + // }) + + // rvsB = rvsB.map((value) => { + // return renderTreeB.getAtomicParent(value) + // }) + + // rvsA = [...Array.from(new Set(rvsA))] + // rvsB = [...Array.from(new Set(rvsB))] + + // const idMapA: { [id: string]: { node: TreeNode; applicationId: string } } = {} + // const appIdMapA: { [id: string]: TreeNode } = {} + // this.buildIdMaps(rvsA, idMapA, appIdMapA) + + // const idMapB: { [id: string]: { node: TreeNode; applicationId: string } } = {} + // const appIdMapB: { [id: string]: TreeNode } = {} + // this.buildIdMaps(rvsB, idMapB, appIdMapB) + + // /** Get the ids which are common between the two maps. This will be objects + // * which have not changed + // */ + // const unchanged: Array = this.intersection(idMapA, idMapB) + // /** We remove the unchanged objects from B and end up with changed + added */ + // const addedModified = _.omit(idMapB, unchanged) + // /** We remove the unchanged objects from A and end up with changed + removed */ + // const removedModified = _.omit(idMapA, unchanged) + // /** We remove the changed objects from B. An object from B is changed if + // * it's application ID exists in A + // */ + // const added = _.omit(addedModified, function (value: { applicationId: string }) { + // return ( + // value.applicationId !== undefined && + // appIdMapA[value.applicationId] !== undefined + // ) + // }) + // /** We remove the changed objects from A. An object from A is changed if + // * it's application ID exists in B + // */ + // const removed = _.omit( + // removedModified, + // function (value: { applicationId: string }) { + // return ( + // value.applicationId !== undefined && + // appIdMapB[value.applicationId] !== undefined + // ) + // } + // ) + // /** We remove the removed objects from A, leaving us only changed objects */ + // const modifiedRemoved = _.omit(removedModified, Object.keys(removed)) + // /** We remove the removed objects from B, leaving us only changed objects */ + // const modifiedAdded = _.omit(addedModified, Object.keys(added)) + + // /** We fill the arrays from here on out */ + // const modifiedOld = (Object.values(modifiedRemoved) as { node: TreeNode }[]).map( + // (value: { node: TreeNode }) => value.node + // ) + // const modifiedNew = (Object.values(modifiedAdded) as { node: TreeNode }[]).map( + // (value: { node: TreeNode }) => value.node + // ) + // diffResult.unchanged.push(...unchanged.map((value) => idMapA[value].node)) + // diffResult.unchanged.push(...unchanged.map((value) => idMapB[value].node)) + // diffResult.removed.push( + // ...(Object.values(removed) as { node: TreeNode }[]).map( + // (value: { node: TreeNode }) => value.node + // ) + // ) + // diffResult.added.push( + // ...(Object.values(added) as { node: TreeNode }[]).map( + // (value: { node: TreeNode }) => value.node + // ) + // ) + + // modifiedOld.forEach((value, index) => { + // value + // diffResult.modified.push([modifiedOld[index], modifiedNew[index]]) + // }) + // return Promise.resolve(diffResult) + // } } diff --git a/packages/viewer/src/modules/extensions/Extension.ts b/packages/viewer/src/modules/extensions/Extension.ts index 011b281731..d40c7b6bc4 100644 --- a/packages/viewer/src/modules/extensions/Extension.ts +++ b/packages/viewer/src/modules/extensions/Extension.ts @@ -1,13 +1,14 @@ -import { IViewer } from '../..' +import type { Constructor } from 'type-fest' +import { type IViewer } from '../..' import EventEmitter from '../EventEmitter' export class Extension extends EventEmitter { - public get inject(): Array Extension> { + public get inject(): Array> { return [] } protected viewer: IViewer - protected _enabled: boolean + protected _enabled: boolean = false public get enabled(): boolean { return this._enabled diff --git a/packages/viewer/src/modules/extensions/FilteringExtension.ts b/packages/viewer/src/modules/extensions/FilteringExtension.ts index dcbfd04912..a3755c4cb8 100644 --- a/packages/viewer/src/modules/extensions/FilteringExtension.ts +++ b/packages/viewer/src/modules/extensions/FilteringExtension.ts @@ -6,19 +6,24 @@ import SpeckleRenderer from '../SpeckleRenderer' import { FilterMaterialType } from '../materials/Materials' import { NodeRenderView } from '../tree/NodeRenderView' import { Extension } from './Extension' -import { TreeNode, WorldTree } from '../tree/WorldTree' -import { IViewer, UpdateFlags, ViewerEvent } from '../../IViewer' -import { +import { type TreeNode, WorldTree } from '../tree/WorldTree' +import { type IViewer, UpdateFlags, ViewerEvent } from '../../IViewer' +import type { NumericPropertyInfo, PropertyInfo, StringPropertyInfo } from '../filtering/PropertyManager' +/** TO DO: Should remove selectedObjects entirely*/ export type FilteringState = { selectedObjects?: string[] hiddenObjects?: string[] isolatedObjects?: string[] - colorGroups?: Record[] + colorGroups?: { + value: string + color: string + ids: string[] + }[] userColorGroups?: { ids: string[]; color: string }[] activePropFilterKey?: string passMin?: number | null @@ -28,12 +33,12 @@ export type FilteringState = { export class FilteringExtension extends Extension { public WTI: WorldTree private Renderer: SpeckleRenderer - private StateKey: string = null + private StateKey: string | undefined = undefined private VisibilityState = new VisibilityState() - private ColorStringFilterState = null - private ColorNumericFilterState = null - private UserspaceColorState = new UserspaceColorState() + private ColorStringFilterState: ColorStringFilterState | null = null + private ColorNumericFilterState: ColorNumericFilterState | null = null + private UserspaceColorState: UserspaceColorState | null = new UserspaceColorState() private CurrentFilteringState: FilteringState = {} as FilteringState public get filteringState(): FilteringState { @@ -55,7 +60,7 @@ export class FilteringExtension extends Extension { public hideObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false, ghost = false ): FilteringState { @@ -70,7 +75,7 @@ export class FilteringExtension extends Extension { public showObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = false ): FilteringState { return this.setVisibilityState( @@ -83,7 +88,7 @@ export class FilteringExtension extends Extension { public isolateObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = true, ghost = true ): FilteringState { @@ -98,7 +103,7 @@ export class FilteringExtension extends Extension { public unIsolateObjects( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, includeDescendants = true, ghost = true ): FilteringState { @@ -113,7 +118,7 @@ export class FilteringExtension extends Extension { private setVisibilityState( objectIds: string[], - stateKey: string = null, + stateKey: string | undefined = undefined, command: Command, includeDescendants = false, ghost = false @@ -143,7 +148,10 @@ export class FilteringExtension extends Extension { } if (command === Command.HIDE || command === Command.ISOLATE) { - const res = objectIds.reduce((acc, curr) => ((acc[curr] = 1), acc), {}) + const res = objectIds.reduce( + (acc: Record, curr: string) => ((acc[curr] = 1), acc), + {} + ) Object.assign(this.VisibilityState.ids, res) } @@ -159,10 +167,10 @@ export class FilteringExtension extends Extension { this.WTI.walk(this.visibilityWalk.bind(this)) if (command === Command.ISOLATE || command === Command.UNISOLATE) { // this.WTI.walk(this.isolationWalk.bind(this)) - const rvMap = {} + const rvMap: Record = {} this.WTI.walk((node: TreeNode) => { if (!node.model.atomic || this.WTI.isRoot(node)) return true - const rvNodes = this.WTI.getRenderTree().getRenderViewNodesForNode(node, node) + const rvNodes = this.WTI.getRenderTree().getRenderViewNodesForNode(node) if (!this.VisibilityState.ids[node.model.raw.id]) { rvNodes.forEach((rvNode: TreeNode) => { rvMap[rvNode.model.id] = rvNode.model.renderView @@ -181,42 +189,28 @@ export class FilteringExtension extends Extension { } private visibilityWalk(node: TreeNode): boolean { - // if (!node.model.atomic) return true if (this.VisibilityState.ids[node.model.id]) { this.VisibilityState.rvs.push( - ...this.WTI.getRenderTree().getRenderViewsForNode(node, node) - ) - } - return true - } - - private isolationWalk(node: TreeNode): boolean { - if (!node.model.atomic || this.WTI.isRoot(node)) return true - const rvs = this.WTI.getRenderTree().getRenderViewsForNode(node, node) - if (!this.VisibilityState.ids[node.model.raw.id]) { - this.VisibilityState.rvs.push(...rvs) - } else { - this.VisibilityState.rvs = this.VisibilityState.rvs.filter( - (rv) => !rvs.includes(rv) + ...this.WTI.getRenderTree().getRenderViewsForNode(node) ) } return true } - public setColorFilter(prop: PropertyInfo, ghost = true) { + public setColorFilter(prop: PropertyInfo, ghost = true): FilteringState { if (prop.type === 'number') { this.ColorStringFilterState = null - this.ColorNumericFilterState = new ColorNumericFilterState() return this.setNumericColorFilter(prop as NumericPropertyInfo, ghost) } if (prop.type === 'string') { this.ColorNumericFilterState = null - this.ColorStringFilterState = new ColorStringFilterState() return this.setStringColorFilter(prop as StringPropertyInfo, ghost) } + return this.filteringState } - private setNumericColorFilter(numProp: NumericPropertyInfo, ghost) { + private setNumericColorFilter(numProp: NumericPropertyInfo, ghost: boolean) { + this.ColorNumericFilterState = new ColorNumericFilterState() this.ColorNumericFilterState.currentProp = numProp const passMin = numProp.passMin || numProp.min @@ -246,7 +240,7 @@ export class FilteringExtension extends Extension { * as in, if there is an id clash (which will happen for instances), the old implementation's indexOf * would return the first value. Here we choose to do the same */ - const matchingIds = {} + const matchingIds: Record = {} for (let k = 0; k < numProp.valueGroups.length; k++) { if (matchingIds[numProp.valueGroups[k].id]) { continue @@ -265,7 +259,7 @@ export class FilteringExtension extends Extension { if (!node.model.atomic || this.WTI.isRoot(node) || this.WTI.isSubtreeRoot(node)) return true - const rvs = this.WTI.getRenderTree().getRenderViewsForNode(node, node) + const rvs = this.WTI.getRenderTree().getRenderViewsForNode(node) const idx = matchingIds[node.model.raw.id] if (!idx) { nonMatchingRvs.push(...rvs) @@ -275,6 +269,7 @@ export class FilteringExtension extends Extension { value: (idx - passMin) / (passMax - passMin) }) } + return true }) this.ColorNumericFilterState.colorGroups = colorGroups @@ -284,21 +279,23 @@ export class FilteringExtension extends Extension { return this.setFilters() } - private setStringColorFilter(stringProp: StringPropertyInfo, ghost) { + private setStringColorFilter(stringProp: StringPropertyInfo, ghost: boolean) { + this.ColorStringFilterState = new ColorStringFilterState() this.ColorStringFilterState.currentProp = stringProp const valueGroupColors: ValueGroupColorItemStringProps[] = [] for (const vg of stringProp.valueGroups) { const col = stc(vg.value) // TODO: smarter way needed. - const entry = { + const entry: ValueGroupColorItemStringProps = { ...vg, color: new Color(col), - rvs: [] + rvs: [], + idMap: {} } /** This is to avoid indexOf inside the walk callback which is ridiculously slow */ - entry['idMap'] = {} + entry.idMap = {} for (let k = 0; k < vg.ids.length; k++) { - entry['idMap'][vg.ids[k]] = 1 + entry.idMap[vg.ids[k]] = 1 } valueGroupColors.push(entry) } @@ -311,17 +308,16 @@ export class FilteringExtension extends Extension { // they are identified as a different category. // 07.05.2023: Attempt on fixing the issue described above. This fixes #1525, but it does // add a bit of overhead. Not 100% sure if it breaks anything else tho' - const nonMatchingMap = {} + const nonMatchingMap: Record = {} this.WTI.walk((node: TreeNode) => { if (!node.model.atomic || this.WTI.isRoot(node) || this.WTI.isSubtreeRoot(node)) { return true } - const vg = valueGroupColors.find((v) => { - return v['idMap'][node.model.raw.id] + const vg = valueGroupColors.find((v: ValueGroupColorItemStringProps) => { + return v.idMap[node.model.raw.id] }) - const rvNodes = this.WTI.getRenderTree().getRenderViewNodesForNode(node, node) - + const rvNodes = this.WTI.getRenderTree().getRenderViewNodesForNode(node) if (!vg) { rvNodes.forEach( (rvNode) => @@ -331,7 +327,7 @@ export class FilteringExtension extends Extension { return true } - const rvs = [] + const rvs: Array = [] rvNodes.forEach((value: TreeNode) => { if (this.WTI.getRenderTree().getAtomicParent(value) === node) { @@ -348,7 +344,10 @@ export class FilteringExtension extends Extension { const nonMatchingRvs: NodeRenderView[] = Object.values(nonMatchingMap) /** Deleting this since we're not going to use it further */ for (const vg of valueGroupColors) { - delete vg['idMap'] + /** Adamant on this one */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + delete vg.idMap } this.ColorStringFilterState.colorGroups = valueGroupColors this.ColorStringFilterState.rampTexture = rampTexture @@ -366,7 +365,9 @@ export class FilteringExtension extends Extension { return this.filteringState } - public setUserObjectColors(groups: { objectIds: string[]; color: string }[]) { + public setUserObjectColors( + groups: { objectIds: string[]; color: string }[] + ): FilteringState { this.UserspaceColorState = new UserspaceColorState() // Resetting any other filtering color ops as they're not compatible this.ColorNumericFilterState = null @@ -388,7 +389,7 @@ export class FilteringExtension extends Extension { group.nodes.push(...nodes) nodes.forEach((node: TreeNode) => { const rvsNodes = this.WTI.getRenderTree() - .getRenderViewNodesForNode(node, node) + .getRenderViewNodesForNode(node) .map((rvNode) => rvNode.model.renderView) if (rvsNodes) group.rvs.push(...rvsNodes) }) @@ -404,17 +405,17 @@ export class FilteringExtension extends Extension { return this.setFilters() } - public removeUserObjectColors() { + public removeUserObjectColors(): FilteringState { this.UserspaceColorState = null return this.setFilters() } - public resetFilters(): FilteringState { + public resetFilters(): FilteringState | null { this.VisibilityState = new VisibilityState() this.ColorStringFilterState = null this.ColorNumericFilterState = null this.UserspaceColorState = null - this.StateKey = null + this.StateKey = undefined this.Renderer.resetMaterials() this.viewer.requestRender(UpdateFlags.RENDER | UpdateFlags.SHADOWS) return null @@ -441,8 +442,9 @@ export class FilteringExtension extends Extension { color: group.color.getHexString(), ids: group.ids }) - this.CurrentFilteringState.activePropFilterKey = - this.ColorStringFilterState.currentProp.key + if (this.ColorStringFilterState.currentProp) + this.CurrentFilteringState.activePropFilterKey = + this.ColorStringFilterState.currentProp.key } } // Number based colors @@ -550,7 +552,9 @@ export class FilteringExtension extends Extension { if (this.idCache[key] && this.idCache[key].length) return this.idCache[key] for (let k = 0; k < objectIds.length; k++) { - const node = this.WTI.findId(objectIds[k])[0] + const nodes = this.WTI.findId(objectIds[k]) + if (!nodes) continue + const node = nodes[0] const subtree = node.all((node) => { return node.model.raw !== undefined }) @@ -597,16 +601,16 @@ class VisibilityState { } class ColorStringFilterState { - public currentProp: StringPropertyInfo + public currentProp: StringPropertyInfo | null public colorGroups: ValueGroupColorItemStringProps[] public nonMatchingRvs: NodeRenderView[] - public rampTexture: Texture + public rampTexture: Texture | undefined public ghost = true public reset() { this.currentProp = null this.colorGroups = [] this.nonMatchingRvs = [] - this.rampTexture = null + this.rampTexture = undefined } } @@ -615,14 +619,15 @@ type ValueGroupColorItemStringProps = { ids: string[] color: Color rvs: NodeRenderView[] + idMap: Record } class ColorNumericFilterState { - public currentProp: NumericPropertyInfo - public nonMatchingRvs: NodeRenderView[] - public colorGroups: ValueGroupColorItemNumericProps[] + public currentProp!: NumericPropertyInfo + public nonMatchingRvs!: NodeRenderView[] + public colorGroups!: ValueGroupColorItemNumericProps[] public ghost = true - public matchingIds: string[] + public matchingIds!: Record } type ValueGroupColorItemNumericProps = { @@ -637,7 +642,7 @@ class UserspaceColorState { nodes: TreeNode[] rvs: NodeRenderView[] }[] = [] - public rampTexture: Texture + public rampTexture!: Texture public reset() { this.groups = [] } diff --git a/packages/viewer/src/modules/extensions/SectionOutlines.ts b/packages/viewer/src/modules/extensions/SectionOutlines.ts index 2a7abe1994..43d15b3872 100644 --- a/packages/viewer/src/modules/extensions/SectionOutlines.ts +++ b/packages/viewer/src/modules/extensions/SectionOutlines.ts @@ -5,6 +5,7 @@ import { Group, InterleavedBufferAttribute, Line3, + Material, Plane, Vector2, Vector3 @@ -15,7 +16,7 @@ import { Geometry } from '../converter/Geometry' import SpeckleGhostMaterial from '../materials/SpeckleGhostMaterial' import SpeckleLineMaterial from '../materials/SpeckleLineMaterial' import { Extension } from './Extension' -import { IViewer } from '../..' +import { type IViewer } from '../..' import { SectionTool, SectionToolEvent } from './SectionTool' import { GeometryType } from '../batching/Batch' import { ObjectLayers } from '../../IViewer' @@ -43,7 +44,6 @@ export class SectionOutlines extends Extension { private static readonly INITIAL_BUFFER_SIZE = 60000 // Must be a multiple of 6 private tmpVec: Vector3 = new Vector3() - private tmpVec2: Vector3 = new Vector3() private up: Vector3 = new Vector3(0, 1, 0) private down: Vector3 = new Vector3(0, -1, 0) private left: Vector3 = new Vector3(-1, 0, 0) @@ -94,7 +94,7 @@ export class SectionOutlines extends Extension { this.sectionProvider.on(SectionToolEvent.Updated, this.sectionUpdated.bind(this)) } - public getPlaneOutline(planeId: PlaneId) { + private getPlaneOutline(planeId: PlaneId) { return this.planeOutlines[planeId] } @@ -129,6 +129,10 @@ export class SectionOutlines extends Extension { const tempVector4 = new Vector3() const tempLine = new Line3() const planeId = this.getPlaneId(_plane) + if (!planeId) { + Logger.error(`Invalid plane! Aborting section outline update`) + return + } const clipOutline = this.planeOutlines[planeId].renderable let index = 0 let posAttr = ( @@ -154,13 +158,17 @@ export class SectionOutlines extends Extension { const localPlane = plane return localPlane.intersectsBox(box) }, - intersectsTriangle(tri, i, contained, depth, batchObject) { - i - contained - depth + intersectsTriangle(tri, _i, _contained, _depth, batchObject) { + /** Catering to typescript */ + /** We're intersecting the AS for meshes. There will always be a batchObject */ + if (!batchObject) { + throw new Error('Null batch object in AS intersection!') + } // check each triangle edge to see if it intersects with the plane. If so then // add it to the list of segments. - const material = batches[b].mesh.getBatchObjectMaterial(batchObject) + const material = batches[b].mesh.getBatchObjectMaterial( + batchObject + ) as Material if ( material instanceof SpeckleGhostMaterial || material.visible === false || @@ -349,9 +357,7 @@ export class SectionOutlines extends Extension { ) for (let k = 0; k < planes.length; k++) { this.updatePlaneOutline( - this.viewer - .getRenderer() - .batcher.getBatches(undefined, GeometryType.MESH) as MeshBatch[], + this.viewer.getRenderer().batcher.getBatches(undefined, GeometryType.MESH), planes[k], outlineOffset ) @@ -374,7 +380,7 @@ export class SectionOutlines extends Extension { Geometry.updateRTEGeometry(outline.renderable.geometry, buffer) } - private getPlaneId(plane: Plane) { + private getPlaneId(plane: Plane): PlaneId | undefined { this.tmpVec.set( Math.round(plane.normal.x), Math.round(plane.normal.y), @@ -386,5 +392,7 @@ export class SectionOutlines extends Extension { if (this.tmpVec.equals(this.down)) return PlaneId.NEGATIVE_Y if (this.tmpVec.equals(this.back)) return PlaneId.NEGATIVE_Z if (this.tmpVec.equals(this.forward)) return PlaneId.POSITIVE_Z + + return undefined } } diff --git a/packages/viewer/src/modules/extensions/SectionTool.ts b/packages/viewer/src/modules/extensions/SectionTool.ts index 561daa9428..7e5ea75858 100644 --- a/packages/viewer/src/modules/extensions/SectionTool.ts +++ b/packages/viewer/src/modules/extensions/SectionTool.ts @@ -13,10 +13,12 @@ import { BufferAttribute, Raycaster, DoubleSide, - SphereGeometry + SphereGeometry, + type Intersection, + Vector2 } from 'three' import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js' -import { IViewer, ObjectLayers } from '../../IViewer' +import { type IViewer, ObjectLayers } from '../../IViewer' import { Extension } from './Extension' import { CameraEvent } from '../objects/SpeckleCamera' import { InputEvent } from '../input/Input' @@ -28,6 +30,12 @@ export enum SectionToolEvent { Updated = 'section-box-changed' } +export interface SectionToolEventPayload { + [SectionToolEvent.DragStart]: void + [SectionToolEvent.DragEnd]: void + [SectionToolEvent.Updated]: Plane[] +} + export class SectionTool extends Extension { public get inject() { return [CameraController] @@ -39,20 +47,20 @@ export class SectionTool extends Extension { protected boxMaterial: MeshStandardMaterial protected boxMesh: Mesh protected boxMeshHelper: Box3Helper - protected boxMeshHelperMaterial: LineBasicMaterial + protected boxMeshHelperMaterial!: LineBasicMaterial protected plane: PlaneGeometry protected hoverPlane: Mesh protected sphere: Mesh protected sidesSimple: { [id: string]: { verts: number[]; axis: string } } - protected currentRange: number[] - protected planes: Plane[] + protected currentRange: number[] | null + protected planes!: Plane[] - protected prevPosition: Vector3 + protected prevPosition: Vector3 | null protected attachedToBox: boolean - protected controls: TransformControls - protected allowSelection: boolean + protected controls!: TransformControls + protected allowSelection!: boolean protected raycaster: Raycaster @@ -68,6 +76,14 @@ export class SectionTool extends Extension { this.viewer.requestRender() } + public get visible(): boolean { + return this.display.visible + } + + public set visible(value: boolean) { + this.display.visible = value + } + constructor(viewer: IViewer, protected cameraProvider: CameraController) { super(viewer) this.viewer = viewer @@ -94,7 +110,7 @@ export class SectionTool extends Extension { this.display.add(this.boxMesh) - this.boxMeshHelper = new Box3Helper(this.boxGeometry.boundingBox) + this.boxMeshHelper = new Box3Helper(this.boxGeometry.boundingBox || new Box3()) this.boxMeshHelper.material = new LineBasicMaterial({ color: 0x0a66ff, opacity: 0.4 @@ -162,15 +178,27 @@ export class SectionTool extends Extension { this.cameraProvider.on(CameraEvent.FrameUpdate, (data: boolean) => { this.allowSelection = !data }) - this.viewer.getRenderer().input.on(InputEvent.Click, this._clickHandler.bind(this)) + this.viewer.getRenderer().input.on(InputEvent.Click, this.clickHandler.bind(this)) this.enabled = false } + public on( + eventType: T, + listener: (arg: SectionToolEventPayload[T]) => void + ): void { + super.on(eventType, listener) + } + private _setupControls() { + const camera = this.viewer.getRenderer().renderingCamera + if (!camera) { + throw new Error('Cannot create SectionTool extension. No rendering camera found') + } + this.controls?.dispose() this.controls?.detach() this.controls = new TransformControls( - this.viewer.getRenderer().renderingCamera, + camera, this.viewer.getRenderer().renderer.domElement ) for (let k = 0; k < this.controls?.children.length; k++) { @@ -203,7 +231,7 @@ export class SectionTool extends Extension { private _draggingChangeHandler() { if (!this.display.visible) return this.boxGeometry.computeBoundingBox() - this.boxMeshHelper.box.copy(this.boxGeometry.boundingBox) + this.boxMeshHelper.box.copy(this.boxGeometry.boundingBox || new Box3()) // Dragging a side / plane if (this.dragging && this.currentRange) { @@ -247,16 +275,16 @@ export class SectionTool extends Extension { this.prevPosition = this.sphere.position.clone() } this.viewer.getRenderer().clippingPlanes = this.planes - this.viewer.getRenderer().clippingVolume = this.getCurrentBox() + this.viewer.getRenderer().clippingVolume = this.getBox() this.emit(SectionToolEvent.Updated, this.planes) this.viewer.requestRender() } - private _clickHandler(args) { + private clickHandler(args: Vector2 & { event: PointerEvent; multiSelect: boolean }) { if (!this.allowSelection || this.dragging) return this.raycaster.setFromCamera(args, this.cameraProvider.renderingCamera) - let intersectedObjects = [] + let intersectedObjects: Array = [] if (this.display.visible) { intersectedObjects = this.raycaster.intersectObject(this.boxMesh) } @@ -272,8 +300,14 @@ export class SectionTool extends Extension { this.hoverPlane.visible = true const side = this.sidesSimple[ - `${intersectedObjects[0].face.a}${intersectedObjects[0].face.b}${intersectedObjects[0].face.c}` + `${intersectedObjects[0].face?.a}${intersectedObjects[0].face?.b}${intersectedObjects[0].face?.c}` ] + /** Catering to typescript + * We're intersection an indexed mesh. There will always be an intersected face + */ + if (!side) { + throw new Error('Cannot determine section side') + } this.controls.showX = side.axis === 'x' this.controls.showY = side.axis === 'y' this.controls.showZ = side.axis === 'z' @@ -413,12 +447,12 @@ export class SectionTool extends Extension { this.controls.showZ = true } - public getCurrentBox() { + public getBox(): Box3 { if (!this.display.visible) return new Box3() - return this.boxGeometry.boundingBox + return this.boxGeometry.boundingBox || new Box3() } - public setBox(targetBox, offset = 0) { + public setBox(targetBox: Box3, offset = 0): void { let box if (targetBox) box = targetBox @@ -479,22 +513,14 @@ export class SectionTool extends Extension { this.boxGeometry.computeBoundingSphere() this._generateOrUpdatePlanes() this._attachControlsToBox() - this.boxMeshHelper.box.copy(this.boxGeometry.boundingBox) + this.boxMeshHelper.box.copy(this.boxGeometry.boundingBox || new Box3()) this.emit(SectionToolEvent.Updated, this.planes) this.viewer.getRenderer().clippingPlanes = this.planes - this.viewer.getRenderer().clippingVolume = this.getCurrentBox() + this.viewer.getRenderer().clippingVolume = this.getBox() this.viewer.requestRender() } - public toggle() { + public toggle(): void { this.enabled = !this._enabled } - - public displayOff() { - this.display.visible = false - } - - public displayOn() { - this.display.visible = true - } } diff --git a/packages/viewer/src/modules/extensions/SelectionExtension.ts b/packages/viewer/src/modules/extensions/SelectionExtension.ts index 025504b33c..5b7624d09d 100644 --- a/packages/viewer/src/modules/extensions/SelectionExtension.ts +++ b/packages/viewer/src/modules/extensions/SelectionExtension.ts @@ -1,20 +1,23 @@ -import { ExtendedIntersection } from '../objects/SpeckleRaycaster' +import { type ExtendedIntersection } from '../objects/SpeckleRaycaster' import { Extension } from './Extension' import { NodeRenderView } from '../tree/NodeRenderView' -import { Material } from 'three' +import { Material, Vector2 } from 'three' import { InputEvent } from '../input/Input' import { MathUtils } from 'three' import { - IViewer, + type IViewer, ObjectLayers, - SelectionEvent, + type SelectionEvent, UpdateFlags, ViewerEvent } from '../../IViewer' -import Materials, { DisplayStyle, RenderMaterial } from '../materials/Materials' +import Materials, { + type DisplayStyle, + type RenderMaterial +} from '../materials/Materials' import { StencilOutlineType } from '../../IViewer' -import { MaterialOptions } from '../materials/MaterialOptions' -import { TreeNode } from '../tree/WorldTree' +import { type MaterialOptions } from '../materials/MaterialOptions' +import { type TreeNode } from '../tree/WorldTree' import { CameraController } from './CameraController' export interface SelectionExtensionOptions { @@ -55,21 +58,23 @@ export class SelectionExtension extends Extension { protected selectedNodes: Array = [] protected selectionRvs: { [id: string]: NodeRenderView } = {} protected selectionMaterials: { [id: string]: Material } = {} - protected options: SelectionExtensionOptions - protected hoverRv: NodeRenderView - protected hoverMaterial: Material - protected selectionMaterialData: RenderMaterial & DisplayStyle & MaterialOptions - protected hoverMaterialData: RenderMaterial & DisplayStyle & MaterialOptions - protected transparentSelectionMaterialData: RenderMaterial & + protected hoverRv!: NodeRenderView | null + protected hoverMaterial!: Material | null + protected selectionMaterialData!: RenderMaterial & DisplayStyle & MaterialOptions + protected hoverMaterialData!: RenderMaterial & DisplayStyle & MaterialOptions + protected transparentSelectionMaterialData!: RenderMaterial & DisplayStyle & MaterialOptions - protected transparentHoverMaterialData: RenderMaterial & + protected transparentHoverMaterialData!: RenderMaterial & + DisplayStyle & + MaterialOptions + protected hiddenSelectionMaterialData!: RenderMaterial & DisplayStyle & MaterialOptions - protected hiddenSelectionMaterialData: RenderMaterial & DisplayStyle & MaterialOptions protected _enabled = true + protected _options!: SelectionExtensionOptions - public get enabled() { + public get enabled(): boolean { return this._enabled } @@ -77,19 +82,12 @@ export class SelectionExtension extends Extension { this._enabled = value } - public constructor(viewer: IViewer, protected cameraProvider: CameraController) { - super(viewer) - this.viewer.on(ViewerEvent.ObjectClicked, this.onObjectClicked.bind(this)) - this.viewer.on(ViewerEvent.ObjectDoubleClicked, this.onObjectDoubleClick.bind(this)) - this.viewer - .getRenderer() - .input.on(InputEvent.PointerMove, this.onPointerMove.bind(this)) - this.setOptions(DefaultSelectionExtensionOptions) + public get options(): SelectionExtensionOptions { + return this._options } - public setOptions(options: SelectionExtensionOptions) { - this.options = options - /** Opaque selection */ + public set options(value: SelectionExtensionOptions) { + this._options = value this.selectionMaterialData = Object.assign({}, this.options.selectionMaterialData) /** Transparent selection */ this.transparentSelectionMaterialData = Object.assign( @@ -114,11 +112,24 @@ export class SelectionExtension extends Extension { this.transparentHoverMaterialData.opacity = 0.5 } - public getSelectedObjects() { + public constructor(viewer: IViewer, protected cameraProvider: CameraController) { + super(viewer) + this.viewer.on(ViewerEvent.ObjectClicked, this.onObjectClicked.bind(this)) + this.viewer.on(ViewerEvent.ObjectDoubleClicked, this.onObjectDoubleClick.bind(this)) + this.viewer + .getRenderer() + .input.on(InputEvent.PointerMove, this.onPointerMove.bind(this)) + this.options = DefaultSelectionExtensionOptions + } + + public getSelectedObjects(): Array> { return this.selectedNodes.map((v) => v.model.raw) } + public getSelectedNodes(): Array { + return this.selectedNodes + } - public selectObjects(ids: Array, multiSelect = false) { + public selectObjects(ids: Array, multiSelect = false): void { if (!this._enabled) return if (!multiSelect) { @@ -126,18 +137,21 @@ export class SelectionExtension extends Extension { } for (let k = 0; k < ids.length; k++) { - this.selectedNodes.push(...this.viewer.getWorldTree().findId(ids[k])) + const foundNodes = this.viewer.getWorldTree().findId(ids[k]) + if (foundNodes) this.selectedNodes.push(...foundNodes) } this.applySelection() } - public unselectObjects(ids: Array) { + /**TO DO: This is redundant */ + public unselectObjects(ids: Array): void { if (!this._enabled) return const nodes = [] for (let k = 0; k < ids.length; k++) { - nodes.push(...this.viewer.getWorldTree().findId(ids[k])) + const foundNodes = this.viewer.getWorldTree().findId(ids[k]) + if (foundNodes) nodes.push(...foundNodes) } this.clearSelection(nodes) } @@ -149,10 +163,10 @@ export class SelectionExtension extends Extension { return } - const rvs = [] + const rvs: Array = [] nodes.forEach((node: TreeNode) => { rvs.push( - ...this.viewer.getWorldTree().getRenderTree().getRenderViewsForNode(node, node) + ...this.viewer.getWorldTree().getRenderTree().getRenderViewsForNode(node) ) }) this.removeSelection(rvs) @@ -162,7 +176,7 @@ export class SelectionExtension extends Extension { ) } - protected onObjectClicked(selection: SelectionEvent) { + protected onObjectClicked(selection: SelectionEvent | null) { if (!this._enabled) return if (!selection) { @@ -177,7 +191,7 @@ export class SelectionExtension extends Extension { this.applySelection() } - protected onObjectDoubleClick(selectionInfo: SelectionEvent) { + protected onObjectDoubleClick(selectionInfo: SelectionEvent | null) { if (!this._enabled) return if (!selectionInfo) { @@ -190,8 +204,10 @@ export class SelectionExtension extends Extension { ) } - protected onPointerMove(e) { + protected onPointerMove(e: Vector2 & { event: Event }) { if (!this._enabled) return + const camera = this.viewer.getRenderer().renderingCamera + if (!camera) return if (!this.options.hoverMaterialData) return const result = @@ -199,16 +215,16 @@ export class SelectionExtension extends Extension { .getRenderer() .intersections.intersect( this.viewer.getRenderer().scene, - this.viewer.getRenderer().renderingCamera, + camera, e, - true, - this.viewer.getRenderer().clippingVolume, [ ObjectLayers.STREAM_CONTENT_MESH, ObjectLayers.STREAM_CONTENT_POINT, ObjectLayers.STREAM_CONTENT_LINE, ObjectLayers.STREAM_CONTENT_TEXT - ] + ], + true, + this.viewer.getRenderer().clippingVolume ) as ExtendedIntersection[]) || [] /* TEMPORARY */ @@ -228,17 +244,21 @@ export class SelectionExtension extends Extension { const rvs = this.viewer .getWorldTree() .getRenderTree() - .getRenderViewsForNode(this.selectedNodes[k], this.selectedNodes[k]) + .getRenderViewsForNode(this.selectedNodes[k]) rvs.forEach((rv: NodeRenderView) => { if (!this.selectionRvs[rv.guid]) this.selectionRvs[rv.guid] = rv - if (!this.selectionMaterials[rv.guid]) - this.selectionMaterials[rv.guid] = this.viewer.getRenderer().getMaterial(rv) + if (!this.selectionMaterials[rv.guid]) { + this.selectionMaterials[rv.guid] = this.viewer + .getRenderer() + .getMaterial(rv) as Material + } }) } const rvs = Object.values(this.selectionRvs) const opaqueRvs = rvs.filter( (value) => + this.selectionMaterials[value.guid] && this.selectionMaterials[value.guid].visible && this.selectionMaterials[value.guid] && !( @@ -248,13 +268,16 @@ export class SelectionExtension extends Extension { ) const transparentRvs = rvs.filter( (value) => + this.selectionMaterials[value.guid] && this.selectionMaterials[value.guid].visible && this.selectionMaterials[value.guid] && this.selectionMaterials[value.guid].transparent && this.selectionMaterials[value.guid].opacity < 1 ) const hiddenRvs = rvs.filter( - (value) => this.selectionMaterials[value.guid].visible === false + (value) => + this.selectionMaterials[value.guid] && + this.selectionMaterials[value.guid].visible === false ) this.viewer.getRenderer().setMaterial(opaqueRvs, this.selectionMaterialData) @@ -268,7 +291,7 @@ export class SelectionExtension extends Extension { protected removeSelection(rvs?: Array) { this.removeHover() - const materialMap = {} + const materialMap: Record = {} rvs = rvs ? rvs : Object.values(this.selectionRvs) rvs.forEach((rv: NodeRenderView) => { const material = this.selectionMaterials[rv.guid] @@ -293,7 +316,7 @@ export class SelectionExtension extends Extension { } } - protected applyHover(renderView: NodeRenderView) { + protected applyHover(renderView: NodeRenderView | null) { this.removeHover() if (!renderView) return @@ -303,7 +326,7 @@ export class SelectionExtension extends Extension { this.removeHover() this.hoverRv = renderView - this.hoverMaterial = this.viewer.getRenderer().getMaterial(this.hoverRv) + this.hoverMaterial = this.viewer.getRenderer().getMaterial(this.hoverRv) as Material this.viewer .getRenderer() .setMaterial( @@ -317,7 +340,7 @@ export class SelectionExtension extends Extension { } protected removeHover() { - if (this.hoverRv) + if (this.hoverRv && this.hoverMaterial) this.viewer.getRenderer().setMaterial([this.hoverRv], this.hoverMaterial) this.hoverRv = null this.hoverMaterial = null diff --git a/packages/viewer/src/modules/extensions/measurements/Measurement.ts b/packages/viewer/src/modules/extensions/measurements/Measurement.ts index 479898f1b2..a4591561ea 100644 --- a/packages/viewer/src/modules/extensions/measurements/Measurement.ts +++ b/packages/viewer/src/modules/extensions/measurements/Measurement.ts @@ -1,6 +1,14 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-empty-function */ -import { Box3, Camera, Object3D, Plane, Vector2, Vector3, Vector4 } from 'three' +import { + Box3, + Camera, + Object3D, + Plane, + Raycaster, + Vector2, + Vector3, + Vector4, + type Intersection +} from 'three' export enum MeasurementState { HIDDEN, @@ -14,8 +22,8 @@ export abstract class Measurement extends Object3D { public endPoint: Vector3 = new Vector3() public startNormal: Vector3 = new Vector3() public endNormal: Vector3 = new Vector3() - public startLineLength: number - public endLineLength: number + public startLineLength!: number + public endLineLength!: number public value = 0 public units = 'm' public precision = 2 @@ -31,7 +39,7 @@ export abstract class Measurement extends Object3D { protected static vec2Buff0: Vector2 = new Vector2() protected _state: MeasurementState = MeasurementState.HIDDEN - protected renderingCamera: Camera + protected renderingCamera: Camera | null protected renderingSize: Vector2 = new Vector2() public set state(value: MeasurementState) { @@ -42,18 +50,19 @@ export abstract class Measurement extends Object3D { return this._state } - public set isVisible(value: boolean) {} + public abstract set isVisible(value: boolean) public get bounds(): Box3 { return new Box3().expandByPoint(this.startPoint).expandByPoint(this.endPoint) } - public frameUpdate(camera: Camera, size: Vector2, bounds: Box3) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public frameUpdate(camera: Camera | null, size: Vector2, _bounds: Box3) { this.renderingCamera = camera this.renderingSize.copy(size) } - public update() {} - public raycast(raycaster, intersects) {} - public highlight(value: boolean) {} - public updateClippingPlanes(planes: Plane[]) {} + public abstract update(): void + public abstract raycast(_raycaster: Raycaster, _intersects: Array): void + public abstract highlight(_value: boolean): void + public abstract updateClippingPlanes(_planes: Plane[]): void } diff --git a/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts b/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts index a9904b31e8..8c3c012c6f 100644 --- a/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts +++ b/packages/viewer/src/modules/extensions/measurements/MeasurementPointGizmo.ts @@ -14,8 +14,10 @@ import { PerspectiveCamera, Plane, Quaternion, + Raycaster, Vector2, - Vector3 + Vector3, + type Intersection } from 'three' import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js' import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js' @@ -90,7 +92,10 @@ export class MeasurementPointGizmo extends Group { material.polygonOffset = true material.polygonOffsetFactor = -5 material.polygonOffsetUnits = 5 - material.opacity = this._style.discOpacity + material.opacity = + this._style.discOpacity !== undefined + ? this._style.discOpacity + : DefaultMeasurementPointGizmoStyle.discOpacity material.transparent = material.opacity < 1 return material } @@ -117,8 +122,11 @@ export class MeasurementPointGizmo extends Group { } lineMaterial.linewidth = 2 lineMaterial.worldUnits = false - lineMaterial.resolution = new Vector2(1513, 1306) - lineMaterial.opacity = this._style.lineOpacity + lineMaterial.resolution = new Vector2(256, 256) + lineMaterial.opacity = + this._style.lineOpacity !== undefined + ? this._style.lineOpacity + : DefaultMeasurementPointGizmoStyle.lineOpacity lineMaterial.transparent = lineMaterial.opacity < 1 lineMaterial.depthTest = false return lineMaterial @@ -129,13 +137,18 @@ export class MeasurementPointGizmo extends Group { { color: color ? color : this._style.pointColor }, ['BILLBOARD_FIXED'] ) - material.opacity = this._style.pointOpacity + material.opacity = + this._style.pointOpacity !== undefined + ? this._style.pointOpacity + : DefaultMeasurementPointGizmoStyle.pointOpacity material.transparent = material.opacity < 1 material.color.convertSRGBToLinear() material.toneMapped = false material.depthTest = false material.billboardPixelHeight = - this._style.pointPixelHeight * window.devicePixelRatio + (this._style.pointPixelHeight !== undefined + ? this._style.pointPixelHeight + : DefaultMeasurementPointGizmoStyle.pointPixelHeight) * window.devicePixelRatio material.userData.billboardPos.value.copy(this.point.position) return material } @@ -151,11 +164,16 @@ export class MeasurementPointGizmo extends Group { ) material.toneMapped = false material.color.convertSRGBToLinear() - material.opacity = this._style.textOpacity + material.opacity = + this._style.textOpacity !== undefined + ? this._style.textOpacity + : DefaultMeasurementPointGizmoStyle.textOpacity material.transparent = material.opacity < 1 material.depthTest = false material.billboardPixelHeight = - this._style.textPixelHeight * window.devicePixelRatio + (this._style.textPixelHeight !== undefined + ? this._style.textPixelHeight + : DefaultMeasurementPointGizmoStyle.textPixelHeight) * window.devicePixelRatio material.userData.billboardPos.value.copy(this.text.position) return material.getDerivedMaterial() @@ -169,7 +187,7 @@ export class MeasurementPointGizmo extends Group { const doublePositions = new Float64Array(geometry.attributes.position.array) Geometry.updateRTEGeometry(geometry, doublePositions) - this.disc = new Mesh(geometry, null) + this.disc = new Mesh(geometry, undefined) this.disc.layers.set(ObjectLayers.MEASUREMENTS) const buffer = new Float64Array(18) @@ -181,7 +199,7 @@ export class MeasurementPointGizmo extends Group { Geometry.updateRTEGeometry(lineGeometry, buffer) - this.line = new LineSegments2(lineGeometry, null) + this.line = new LineSegments2(lineGeometry, undefined) this.line.computeLineDistances() this.line.name = `test-mesurements-line` this.line.frustumCulled = false @@ -190,7 +208,7 @@ export class MeasurementPointGizmo extends Group { const sphereGeometry = new CircleGeometry(1, 16) - this.point = new Mesh(sphereGeometry, null) + this.point = new Mesh(sphereGeometry, undefined) this.point.layers.set(ObjectLayers.MEASUREMENTS) this.point.visible = false this.point.renderOrder = 1 @@ -198,7 +216,10 @@ export class MeasurementPointGizmo extends Group { const point2 = new Mesh(sphereGeometry, this.getPointMaterial(0xffffff)) point2.renderOrder = 2 point2.material.billboardPixelHeight = - this._style.pointPixelHeight * window.devicePixelRatio - + (this._style.pointPixelHeight !== undefined + ? this._style.pointPixelHeight + : DefaultMeasurementPointGizmoStyle.pointPixelHeight) * + window.devicePixelRatio - 2 * window.devicePixelRatio point2.layers.set(ObjectLayers.MEASUREMENTS) this.point.add(point2) @@ -211,7 +232,7 @@ export class MeasurementPointGizmo extends Group { this.add(this.line) this.add(this.text) - this.style = style + this.style = style ? style : DefaultMeasurementPointGizmoStyle } public enable(disc: boolean, line: boolean, point: boolean, text: boolean) { @@ -224,7 +245,12 @@ export class MeasurementPointGizmo extends Group { } public frameUpdate(camera: Camera, bounds: Box3) { - if (camera.type === 'PerspectiveCamera' && +this._style.fixedSize > 0) { + if ( + camera.type === 'PerspectiveCamera' && + +(this._style.fixedSize !== undefined + ? this._style.fixedSize + : DefaultMeasurementPointGizmoStyle.fixedSize) > 0 + ) { const cam = camera as PerspectiveCamera const cameraObjectDistance = cam.position.distanceTo(this.disc.position) const worldSize = Math.abs(2 * Math.tan(cam.fov / 2.0) * cameraObjectDistance) @@ -233,7 +259,12 @@ export class MeasurementPointGizmo extends Group { this.disc.scale.set(size, size, size) this.disc.matrixWorldNeedsUpdate = true } - if (camera.type === 'OrthographicCamera' && +this._style.fixedSize > 0) { + if ( + camera.type === 'OrthographicCamera' && + +(this._style.fixedSize !== undefined + ? this._style.fixedSize + : DefaultMeasurementPointGizmoStyle.fixedSize) > 0 + ) { const cam = camera as OrthographicCamera const orthoSize = cam.top - cam.bottom const size = (orthoSize / cam.zoom) * 0.0075 @@ -309,7 +340,7 @@ export class MeasurementPointGizmo extends Group { backgroundPixelHeight: 20 } this.text.setTransform(position, quaternion, scale) - this.text.backgroundMesh.renderOrder = 3 + if (this.text.backgroundMesh) this.text.backgroundMesh.renderOrder = 3 this.text.textMesh.renderOrder = 4 }) } @@ -321,7 +352,7 @@ export class MeasurementPointGizmo extends Group { this.text.textMesh.material = this.getTextMaterial() } - public raycast(raycaster, intersects) { + public raycast(raycaster: Raycaster, intersects: Array) { // this.disc.raycast(raycaster, intersects) this.line.raycast(raycaster, intersects) // this.point.raycast(raycaster, intersects) diff --git a/packages/viewer/src/modules/extensions/measurements/MeasurementsExtension.ts b/packages/viewer/src/modules/extensions/measurements/MeasurementsExtension.ts index d3f7bd7a24..b29e1f06d4 100644 --- a/packages/viewer/src/modules/extensions/measurements/MeasurementsExtension.ts +++ b/packages/viewer/src/modules/extensions/measurements/MeasurementsExtension.ts @@ -1,13 +1,12 @@ import SpeckleRenderer from '../../SpeckleRenderer' -import { IViewer, ObjectLayers } from '../../../IViewer' +import { type IViewer, ObjectLayers } from '../../../IViewer' import { PerpendicularMeasurement } from './PerpendicularMeasurement' import { Plane, Ray, Raycaster, Vector2, Vector3 } from 'three' import { PointToPointMeasurement } from './PointToPointMeasurement' import { Measurement, MeasurementState } from './Measurement' -import { ExtendedIntersection } from '../../objects/SpeckleRaycaster' +import { ExtendedMeshIntersection } from '../../objects/SpeckleRaycaster' import Logger from 'js-logger' -import SpeckleMesh from '../../objects/SpeckleMesh' import SpeckleGhostMaterial from '../../materials/SpeckleGhostMaterial' import { Extension } from '../Extension' import { InputEvent } from '../../input/Input' @@ -39,12 +38,12 @@ export class MeasurementsExtension extends Extension { return [CameraController] } - protected renderer: SpeckleRenderer = null + protected renderer: SpeckleRenderer protected measurements: Measurement[] = [] - protected _activeMeasurement: Measurement = null - protected _selectedMeasurement: Measurement = null - protected raycaster: Raycaster = null + protected _activeMeasurement: Measurement | null = null + protected _selectedMeasurement: Measurement | null = null + protected raycaster: Raycaster protected _options: MeasurementOptions = Object.assign({}, DefaultMeasurementsOptions) private _frameLock = false @@ -60,10 +59,6 @@ export class MeasurementsExtension extends Extension { return this._enabled } - public get visible(): boolean { - return this._options.visible - } - public set enabled(value: boolean) { this._enabled = value if (this._activeMeasurement) { @@ -75,8 +70,8 @@ export class MeasurementsExtension extends Extension { this.renderer.resetPipeline() } - public set paused(value: boolean) { - this._paused = value + public get options(): MeasurementOptions { + return this._options } public set options(options: MeasurementOptions) { @@ -92,11 +87,11 @@ export class MeasurementsExtension extends Extension { this.applyOptions() } - public get selectedMeasurement(): Measurement { + public get selectedMeasurement(): Measurement | null { return this._selectedMeasurement } - public get activeMeasurement(): Measurement { + public get activeMeasurement(): Measurement | null { return this._activeMeasurement } @@ -111,25 +106,22 @@ export class MeasurementsExtension extends Extension { this.renderer.input.on(InputEvent.DoubleClick, this.onPointerDoubleClick.bind(this)) } - public onLateUpdate(deltaTime: number) { - deltaTime + public onLateUpdate() { if (!this._enabled) return + const camera = this.renderer.renderingCamera + if (!camera) return this._frameLock = false this.renderer.renderer.getDrawingBufferSize(this.screenBuff0) if (this._activeMeasurement) this._activeMeasurement.frameUpdate( - this.renderer.renderingCamera, + camera, this.screenBuff0, this.renderer.sceneBox ) this.measurements.forEach((value: Measurement) => { - value.frameUpdate( - this.renderer.renderingCamera, - this.screenBuff0, - this.renderer.sceneBox - ) + value.frameUpdate(camera, this.screenBuff0, this.renderer.sceneBox) }) } @@ -137,28 +129,29 @@ export class MeasurementsExtension extends Extension { this.renderer.renderer.getDrawingBufferSize(this.screenBuff0) } - protected onPointerMove(data) { + protected onPointerMove(data: Vector2 & { event: Event }) { if (!this._enabled || this._paused) return + const camera = this.renderer.renderingCamera + if (!camera) return + if (this._frameLock) { return } - let result = - (this.renderer.intersections.intersect( + let result: ExtendedMeshIntersection[] = + this.renderer.intersections.intersect( this.renderer.scene, - this.renderer.renderingCamera, + camera, data, + ObjectLayers.STREAM_CONTENT_MESH, true, - this.renderer.clippingVolume, - [ObjectLayers.STREAM_CONTENT_MESH] - ) as ExtendedIntersection[]) || [] + this.renderer.clippingVolume + ) || [] - result = result.filter((value: ExtendedIntersection) => { - const material = (value.object as unknown as SpeckleMesh).getBatchObjectMaterial( - value.batchObject - ) - return !(material instanceof SpeckleGhostMaterial) && material.visible + result = result.filter((value: ExtendedMeshIntersection) => { + const material = value.object.getBatchObjectMaterial(value.batchObject) + return material && !(material instanceof SpeckleGhostMaterial) && material.visible }) if (!result.length) { @@ -166,16 +159,21 @@ export class MeasurementsExtension extends Extension { return } - if (!this._activeMeasurement) { - this.startMeasurement() - } - this._activeMeasurement.isVisible = true - + /** Catering to typescript + * There will always be an intersected face. We're casting against indexed meshes only + */ this.pointBuff.copy(result[0].point) this.normalBuff.copy(result[0].face.normal) + if (this._options.vertexSnap) { this.snap(result[0], this.pointBuff, this.normalBuff) } + + if (!this._activeMeasurement) { + this._activeMeasurement = this.startMeasurement() + this._activeMeasurement.isVisible = true + } + if (this._activeMeasurement.state === MeasurementState.DANGLING_START) { this._activeMeasurement.startPoint.copy(this.pointBuff) this._activeMeasurement.startNormal.copy(this.normalBuff) @@ -189,9 +187,12 @@ export class MeasurementsExtension extends Extension { this.renderer.resetPipeline() this._frameLock = true this._sceneHit = true + // console.log('Time -> ', performance.now() - start) } - protected onPointerClick(data) { + protected onPointerClick( + data: { event: PointerEvent; multiSelect: boolean } & Vector2 + ) { if (!this._enabled) return const measurement = this.pickMeasurement(data) @@ -216,7 +217,9 @@ export class MeasurementsExtension extends Extension { } } - protected onPointerDoubleClick(data) { + protected onPointerDoubleClick( + data: Vector2 & { event: PointerEvent; multiSelect: boolean } + ) { const measurement = this.pickMeasurement(data) if (measurement) { this.cameraProvider.setCameraView(measurement.bounds, true) @@ -228,25 +231,24 @@ export class MeasurementsExtension extends Extension { } } - protected autoLazerMeasure(data) { + protected autoLazerMeasure(data: Vector2) { if (!this._activeMeasurement) return + if (!this.renderer.renderingCamera) return this._activeMeasurement.state = MeasurementState.DANGLING_START - let result = - (this.renderer.intersections.intersect( + let result: ExtendedMeshIntersection[] = + this.renderer.intersections.intersect( this.renderer.scene, this.renderer.renderingCamera, data, + ObjectLayers.STREAM_CONTENT_MESH, true, - this.renderer.clippingVolume, - [ObjectLayers.STREAM_CONTENT_MESH] - ) as ExtendedIntersection[]) || [] + this.renderer.clippingVolume + ) || [] result = result.filter((value) => { - const material = (value.object as unknown as SpeckleMesh).getBatchObjectMaterial( - value.batchObject - ) - return !(material instanceof SpeckleGhostMaterial) && material.visible + const material = value.object.getBatchObjectMaterial(value.batchObject) + return material && !(material instanceof SpeckleGhostMaterial) && material.visible }) if (!result.length) return @@ -257,21 +259,19 @@ export class MeasurementsExtension extends Extension { const offsetPoint = new Vector3() .copy(startPoint) .add(new Vector3().copy(startNormal).multiplyScalar(0.000001)) - let perpResult = - (this.renderer.intersections.intersectRay( + let perpResult: ExtendedMeshIntersection[] = + this.renderer.intersections.intersectRay( this.renderer.scene, this.renderer.renderingCamera, new Ray(offsetPoint, startNormal), + ObjectLayers.STREAM_CONTENT_MESH, true, - this.renderer.clippingVolume, - [ObjectLayers.STREAM_CONTENT_MESH] - ) as ExtendedIntersection[]) || [] + this.renderer.clippingVolume + ) || [] - perpResult = perpResult.filter((value) => { - const material = (value.object as unknown as SpeckleMesh).getBatchObjectMaterial( - value.batchObject - ) - return !(material instanceof SpeckleGhostMaterial) && material.visible + perpResult = perpResult.filter((value: ExtendedMeshIntersection) => { + const material = value.object.getBatchObjectMaterial(value.batchObject) + return material && !(material instanceof SpeckleGhostMaterial) && material.visible }) if (!perpResult.length) { @@ -288,29 +288,34 @@ export class MeasurementsExtension extends Extension { this.finishMeasurement() } - protected startMeasurement() { + protected startMeasurement(): Measurement { + let measurement: Measurement if (this._options.type === MeasurementType.PERPENDICULAR) - this._activeMeasurement = new PerpendicularMeasurement() + measurement = new PerpendicularMeasurement() else if (this._options.type === MeasurementType.POINTTOPOINT) - this._activeMeasurement = new PointToPointMeasurement() + measurement = new PointToPointMeasurement() + else throw new Error('Unsupported measurement type!') - this._activeMeasurement.state = MeasurementState.DANGLING_START - this._activeMeasurement.frameUpdate( + measurement.state = MeasurementState.DANGLING_START + measurement.frameUpdate( this.renderer.renderingCamera, this.screenBuff0, this.renderer.sceneBox ) - this.renderer.scene.add(this._activeMeasurement) + this.renderer.scene.add(measurement) + return measurement } protected cancelMeasurement() { - this.renderer.scene.remove(this._activeMeasurement) + if (this._activeMeasurement) this.renderer.scene.remove(this._activeMeasurement) this._activeMeasurement = null this.renderer.needsRender = true this.renderer.resetPipeline() } protected finishMeasurement() { + if (!this._activeMeasurement) return + this._activeMeasurement.state = MeasurementState.COMPLETE this._activeMeasurement.update() if (this._activeMeasurement.value > 0) { @@ -334,7 +339,7 @@ export class MeasurementsExtension extends Extension { } } - public clearMeasurements() { + public clearMeasurements(): void { this.removeMeasurement() this.measurements.forEach((measurement: Measurement) => { this.renderer.scene.remove(measurement) @@ -347,16 +352,20 @@ export class MeasurementsExtension extends Extension { let flashCount = 0 const maxFlashCount = 5 const handle = setInterval(() => { - this._activeMeasurement.highlight(Boolean(flashCount++ % 2)) - if (flashCount >= maxFlashCount) { - clearInterval(handle) + if (this._activeMeasurement) { + this._activeMeasurement.highlight(Boolean(flashCount++ % 2)) + if (flashCount >= maxFlashCount) { + clearInterval(handle) + } + this.renderer.needsRender = true + this.renderer.resetPipeline() } - this.renderer.needsRender = true - this.renderer.resetPipeline() }, 100) } - protected pickMeasurement(data): Measurement { + protected pickMeasurement(data: Vector2): Measurement | null { + if (!this.renderer.renderingCamera) return null + this.measurements.forEach((value) => { value.highlight(false) }) @@ -372,10 +381,12 @@ export class MeasurementsExtension extends Extension { } protected snap( - intersection: ExtendedIntersection, + intersection: ExtendedMeshIntersection, outPoint: Vector3, outNormal: Vector3 ) { + if (!this.renderer.renderingCamera) return + const v0 = intersection.batchObject.accelerationStructure .getVertexAtIndex(intersection.face.a) .project(this.renderer.renderingCamera) @@ -407,7 +418,7 @@ export class MeasurementsExtension extends Extension { } } - protected updateClippingPlanes(planes: Plane[]) { + protected updateClippingPlanes(planes: Plane[]): void { this.measurements.forEach((value) => { value.updateClippingPlanes(planes) }) @@ -417,8 +428,14 @@ export class MeasurementsExtension extends Extension { const all = [this._activeMeasurement, ...this.measurements] all.forEach((value) => { if (value) { - value.units = this._options.units - value.precision = this._options.precision + value.units = + this._options.units !== undefined + ? this._options.units + : DefaultMeasurementsOptions.units + value.precision = + this._options.precision !== undefined + ? this._options.precision + : DefaultMeasurementsOptions.precision value.update() } }) @@ -441,6 +458,6 @@ export class MeasurementsExtension extends Extension { measurement.update() measurement.state = MeasurementState.COMPLETE measurement.update() - this.measurements.push(this._activeMeasurement) + this.measurements.push(measurement) } } diff --git a/packages/viewer/src/modules/extensions/measurements/PerpendicularMeasurement.ts b/packages/viewer/src/modules/extensions/measurements/PerpendicularMeasurement.ts index 7279440bb2..c4a53733bb 100644 --- a/packages/viewer/src/modules/extensions/measurements/PerpendicularMeasurement.ts +++ b/packages/viewer/src/modules/extensions/measurements/PerpendicularMeasurement.ts @@ -1,18 +1,26 @@ -import { Box3, Camera, Plane, Vector2, Vector3 } from 'three' +import { + Box3, + Camera, + Plane, + Raycaster, + Vector2, + Vector3, + type Intersection +} from 'three' import { MeasurementPointGizmo } from './MeasurementPointGizmo' import { getConversionFactor } from '../../converter/Units' import { Measurement, MeasurementState } from './Measurement' import { ObjectLayers } from '../../../IViewer' export class PerpendicularMeasurement extends Measurement { - private startGizmo: MeasurementPointGizmo = null - private endGizmo: MeasurementPointGizmo = null + private startGizmo: MeasurementPointGizmo | null = null + private endGizmo: MeasurementPointGizmo | null = null private midPoint: Vector3 = new Vector3() private normalIndicatorPixelSize = 15 * window.devicePixelRatio public set isVisible(value: boolean) { - this.startGizmo.enable(value, value, value, value) - this.endGizmo.enable(value, value, value, value) + this.startGizmo?.enable(value, value, value, value) + this.endGizmo?.enable(value, value, value, value) } public get bounds(): Box3 { @@ -35,8 +43,8 @@ export class PerpendicularMeasurement extends Measurement { public frameUpdate(camera: Camera, size: Vector2, bounds: Box3) { super.frameUpdate(camera, size, bounds) - this.startGizmo.frameUpdate(camera, bounds) - this.endGizmo.frameUpdate(camera, bounds) + this.startGizmo?.frameUpdate(camera, bounds) + this.endGizmo?.frameUpdate(camera, bounds) /** Not a fan of this but the camera library fails to tell us when zooming happens * so we need to update the screen space normal indicator each frame, otherwise it * won't look correct while zooming @@ -48,10 +56,11 @@ export class PerpendicularMeasurement extends Measurement { public update() { if (isNaN(this.startPoint.length())) return + if (!this.renderingCamera) return - this.startGizmo.updateDisc(this.startPoint, this.startNormal) - this.startGizmo.updatePoint(this.startPoint) - this.endGizmo.updateDisc(this.endPoint, this.endNormal) + this.startGizmo?.updateDisc(this.startPoint, this.startNormal) + this.startGizmo?.updatePoint(this.startPoint) + this.endGizmo?.updateDisc(this.endPoint, this.endNormal) if (this._state === MeasurementState.DANGLING_START) { const startLine0 = Measurement.vec3Buff0.copy(this.startPoint) @@ -98,12 +107,12 @@ export class PerpendicularMeasurement extends Measurement { endNDC .applyMatrix4(this.renderingCamera.projectionMatrixInverse) .applyMatrix4(this.renderingCamera.matrixWorld) - this.startGizmo.updateLine([ + this.startGizmo?.updateLine([ startLine0, Measurement.vec3Buff1.set(endNDC.x, endNDC.y, endNDC.z) ]) - this.endGizmo.enable(false, false, false, false) + this.endGizmo?.enable(false, false, false, false) } if (this._state === MeasurementState.DANGLING_END) { @@ -148,11 +157,11 @@ export class PerpendicularMeasurement extends Measurement { .copy(this.startNormal) .multiplyScalar(this.startLineLength) ) - this.startGizmo.updateLine([startLine0, startLine1]) + this.startGizmo?.updateLine([startLine0, startLine1]) const endLine0 = Measurement.vec3Buff3.copy(this.endPoint) - this.endGizmo.updateLine([ + this.endGizmo?.updateLine([ endLine0, endLine3, endLine3, @@ -160,7 +169,7 @@ export class PerpendicularMeasurement extends Measurement { this.midPoint, endLine0 ]) - this.endGizmo.updatePoint(this.midPoint) + this.endGizmo?.updatePoint(this.midPoint) const textPos = Measurement.vec3Buff0 .copy(this.startPoint) @@ -171,29 +180,29 @@ export class PerpendicularMeasurement extends Measurement { ) this.value = this.midPoint.distanceTo(this.startPoint) - this.startGizmo.updateText( + this.startGizmo?.updateText( `${(this.value * getConversionFactor('m', this.units)).toFixed( this.precision )} ${this.units}`, textPos ) - this.endGizmo.enable(true, true, true, true) + this.endGizmo?.enable(true, true, true, true) } if (this._state === MeasurementState.COMPLETE) { - this.startGizmo.updateText( + this.startGizmo?.updateText( `${(this.value * getConversionFactor('m', this.units)).toFixed( this.precision )} ${this.units}` ) - this.startGizmo.enable(false, true, true, true) - this.endGizmo.enable(false, false, true, false) + this.startGizmo?.enable(false, true, true, true) + this.endGizmo?.enable(false, false, true, false) } } - public raycast(raycaster, intersects) { - const results = [] - this.startGizmo.raycast(raycaster, results) - this.endGizmo.raycast(raycaster, results) + public raycast(raycaster: Raycaster, intersects: Array) { + const results: Array = [] + this.startGizmo?.raycast(raycaster, results) + this.endGizmo?.raycast(raycaster, results) if (results.length) { intersects.push({ distance: results[0].distance, @@ -207,12 +216,12 @@ export class PerpendicularMeasurement extends Measurement { } public highlight(value: boolean) { - this.startGizmo.highlight = value - this.endGizmo.highlight = value + if (this.startGizmo) this.startGizmo.highlight = value + if (this.endGizmo) this.endGizmo.highlight = value } public updateClippingPlanes(planes: Plane[]) { - this.startGizmo.updateClippingPlanes(planes) - this.endGizmo.updateClippingPlanes(planes) + if (this.startGizmo) this.startGizmo.updateClippingPlanes(planes) + if (this.endGizmo) this.endGizmo.updateClippingPlanes(planes) } } diff --git a/packages/viewer/src/modules/extensions/measurements/PointToPointMeasurement.ts b/packages/viewer/src/modules/extensions/measurements/PointToPointMeasurement.ts index 2cdb4a00e6..e0bf6fe63e 100644 --- a/packages/viewer/src/modules/extensions/measurements/PointToPointMeasurement.ts +++ b/packages/viewer/src/modules/extensions/measurements/PointToPointMeasurement.ts @@ -1,16 +1,16 @@ -import { Box3, Camera, Plane, Vector2 } from 'three' +import { Box3, Camera, Plane, Raycaster, Vector2, type Intersection } from 'three' import { MeasurementPointGizmo } from './MeasurementPointGizmo' import { getConversionFactor } from '../../converter/Units' import { Measurement, MeasurementState } from './Measurement' import { ObjectLayers } from '../../../IViewer' export class PointToPointMeasurement extends Measurement { - private startGizmo: MeasurementPointGizmo = null - private endGizmo: MeasurementPointGizmo = null + private startGizmo: MeasurementPointGizmo | null = null + private endGizmo: MeasurementPointGizmo | null = null public set isVisible(value: boolean) { - this.startGizmo.enable(value, value, value, value) - this.endGizmo.enable(value, value, value, value) + this.startGizmo?.enable(value, value, value, value) + this.endGizmo?.enable(value, value, value, value) } public constructor() { @@ -26,14 +26,14 @@ export class PointToPointMeasurement extends Measurement { public frameUpdate(camera: Camera, size: Vector2, bounds: Box3) { super.frameUpdate(camera, size, bounds) - this.startGizmo.frameUpdate(camera, bounds) - this.endGizmo.frameUpdate(camera, bounds) + this.startGizmo?.frameUpdate(camera, bounds) + this.endGizmo?.frameUpdate(camera, bounds) } public update() { - this.startGizmo.updateDisc(this.startPoint, this.startNormal) - this.startGizmo.updatePoint(this.startPoint) - this.endGizmo.updateDisc(this.endPoint, this.endNormal) + this.startGizmo?.updateDisc(this.startPoint, this.startNormal) + this.startGizmo?.updatePoint(this.startPoint) + this.endGizmo?.updateDisc(this.endPoint, this.endNormal) if (this._state === MeasurementState.DANGLING_START) { const startLine0 = Measurement.vec3Buff0.copy(this.startPoint) @@ -44,8 +44,8 @@ export class PointToPointMeasurement extends Measurement { .copy(this.startNormal) .multiplyScalar(this.startLineLength) ) - this.startGizmo.updateLine([startLine0, startLine1]) - this.endGizmo.enable(false, false, false, false) + this.startGizmo?.updateLine([startLine0, startLine1]) + this.endGizmo?.enable(false, false, false, false) } if (this._state === MeasurementState.DANGLING_END) { this.startLineLength = this.startPoint.distanceTo(this.endPoint) @@ -69,20 +69,20 @@ export class PointToPointMeasurement extends Measurement { .multiplyScalar(this.startLineLength * 0.5) ) - this.startGizmo.updateLine([this.startPoint, lineEndPoint]) - this.endGizmo.updatePoint(lineEndPoint) - this.startGizmo.updateText( + this.startGizmo?.updateLine([this.startPoint, lineEndPoint]) + this.endGizmo?.updatePoint(lineEndPoint) + this.startGizmo?.updateText( `${(this.value * getConversionFactor('m', this.units)).toFixed( this.precision )} ${this.units}`, textPos ) - this.endGizmo.enable(true, true, true, true) + this.endGizmo?.enable(true, true, true, true) } if (this._state === MeasurementState.COMPLETE) { - this.startGizmo.enable(false, true, true, true) - this.endGizmo.enable(false, false, true, false) - this.startGizmo.updateText( + this.startGizmo?.enable(false, true, true, true) + this.endGizmo?.enable(false, false, true, false) + this.startGizmo?.updateText( `${(this.value * getConversionFactor('m', this.units)).toFixed( this.precision )} ${this.units}` @@ -90,10 +90,10 @@ export class PointToPointMeasurement extends Measurement { } } - public raycast(raycaster, intersects) { - const results = [] - this.startGizmo.raycast(raycaster, results) - this.endGizmo.raycast(raycaster, results) + public raycast(raycaster: Raycaster, intersects: Array) { + const results: Array = [] + this.startGizmo?.raycast(raycaster, results) + this.endGizmo?.raycast(raycaster, results) if (results.length) { intersects.push({ distance: results[0].distance, @@ -107,12 +107,12 @@ export class PointToPointMeasurement extends Measurement { } public highlight(value: boolean) { - this.startGizmo.highlight = value - this.endGizmo.highlight = value + if (this.startGizmo) this.startGizmo.highlight = value + if (this.endGizmo) this.endGizmo.highlight = value } public updateClippingPlanes(planes: Plane[]) { - this.startGizmo.updateClippingPlanes(planes) - this.endGizmo.updateClippingPlanes(planes) + if (this.startGizmo) this.startGizmo.updateClippingPlanes(planes) + if (this.endGizmo) this.endGizmo.updateClippingPlanes(planes) } } diff --git a/packages/viewer/src/modules/filtering/PropertyManager.ts b/packages/viewer/src/modules/filtering/PropertyManager.ts index 46dfbd5148..d696ca6c15 100644 --- a/packages/viewer/src/modules/filtering/PropertyManager.ts +++ b/packages/viewer/src/modules/filtering/PropertyManager.ts @@ -1,5 +1,5 @@ import flatten from '../../helpers/flatten' -import { TreeNode, WorldTree } from '../tree/WorldTree' +import { type TreeNode, WorldTree } from '../tree/WorldTree' export class PropertyManager { private propCache = {} as Record @@ -12,7 +12,7 @@ export class PropertyManager { */ public async getProperties( tree: WorldTree, - resourceUrl: string = null, + resourceUrl: string | null = null, bypassCache = false ): Promise { let rootNode: TreeNode = tree.root @@ -21,14 +21,16 @@ export class PropertyManager { return this.propCache[resourceUrl ? resourceUrl : rootNode.model.id] if (resourceUrl) { - const actualRoot = rootNode.children.find((n) => n.model.id === resourceUrl) + const actualRoot = rootNode.children.find( + (n: { model: { id: string } }) => n.model.id === resourceUrl + ) if (actualRoot) rootNode = actualRoot else { throw new Error(`Could not find root node for ${resourceUrl} - is it loaded?`) } } - const propValues = {} + const propValues: { [key: string]: unknown } = {} await tree.walkAsync((node: TreeNode) => { if (!node.model.atomic) return true @@ -36,7 +38,7 @@ export class PropertyManager { for (const key in obj) { if (Array.isArray(obj[key])) continue if (!propValues[key]) propValues[key] = [] - propValues[key].push({ value: obj[key], id: obj.id }) + ;(propValues[key] as Array).push({ value: obj[key], id: obj.id }) } return true }, rootNode) @@ -47,20 +49,33 @@ export class PropertyManager { const propValuesArr = propValues[propKey] const propInfo = {} as PropertyInfo propInfo.key = propKey + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore propInfo.type = typeof propValuesArr[0].value === 'string' ? 'string' : 'number' + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore propInfo.objectCount = propValuesArr.length // For string based props, keep track of which ids belong to which group if (propInfo.type === 'string') { const stringPropInfo = propInfo as StringPropertyInfo const valueGroups = {} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore for (const { value, id } of propValuesArr) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore if (!valueGroups[value]) valueGroups[value] = [] + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore valueGroups[value].push(id) } stringPropInfo.valueGroups = [] - for (const key in valueGroups) + for (const key in valueGroups) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore stringPropInfo.valueGroups.push({ value: key, ids: valueGroups[key] }) + } stringPropInfo.valueGroups = stringPropInfo.valueGroups.sort((a, b) => a.value.localeCompare(b.value) @@ -71,10 +86,14 @@ export class PropertyManager { const numProp = propInfo as NumericPropertyInfo numProp.min = Number.MAX_VALUE numProp.max = Number.MIN_VALUE + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore for (const { value } of propValuesArr) { if (value < numProp.min) numProp.min = value if (value > numProp.max) numProp.max = value } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore numProp.valueGroups = propValuesArr.sort((a, b) => a.value - b.value) // const sorted = propValuesArr.sort((a, b) => a.value - b.value) // propInfo.sortedValues = sorted.map(s => s.value) diff --git a/packages/viewer/src/modules/input/Input.ts b/packages/viewer/src/modules/input/Input.ts index 17bf0e5cd1..7b731bcbd6 100644 --- a/packages/viewer/src/modules/input/Input.ts +++ b/packages/viewer/src/modules/input/Input.ts @@ -1,38 +1,39 @@ import { Vector2 } from 'three' import EventEmitter from '../EventEmitter' -export interface InputOptions { - hover: boolean -} - -export const InputOptionsDefault = { - hover: false +export enum InputEvent { + PointerDown = 'pointer-down', + PointerUp = 'pointer-up', + PointerMove = 'pointer-move', + Click = 'click', + DoubleClick = 'double-click', + KeyUp = 'key-up' } -export enum InputEvent { - PointerDown, - PointerUp, - PointerMove, - Click, - DoubleClick, - KeyUp +export interface InputEventPayload { + [InputEvent.PointerDown]: Vector2 & { event: PointerEvent } + [InputEvent.PointerUp]: Vector2 & { event: PointerEvent } + [InputEvent.PointerMove]: Vector2 & { event: PointerEvent } + [InputEvent.Click]: Vector2 & { event: PointerEvent; multiSelect: boolean } + [InputEvent.DoubleClick]: Vector2 & { event: PointerEvent; multiSelect: boolean } + [InputEvent.KeyUp]: KeyboardEvent } +//TO DO: Define proper interface for InputEvent data export default class Input extends EventEmitter { private static readonly MAX_DOUBLE_CLICK_TIMING = 500 - private tapTimeout + private tapTimeout: number = 0 private lastTap = 0 private lastClick = 0 - private touchLocation: Touch + private touchLocation: Touch | undefined private container - constructor(container: HTMLElement, _options: InputOptions) { + constructor(container: HTMLElement) { super() - _options this.container = container // Handle mouseclicks - let mdTime + let mdTime: number this.container.addEventListener('pointerdown', (e) => { e.preventDefault() const loc = this._getNormalisedClickPosition(e) @@ -72,10 +73,14 @@ export default class Input extends EventEmitter { const tapLength = currentTime - this.lastTap clearTimeout(this.tapTimeout) if (tapLength < 500 && tapLength > 0) { - const loc = this._getNormalisedClickPosition(this.touchLocation) - this.emit(InputEvent.DoubleClick, loc) + if (this.touchLocation) { + const loc = this._getNormalisedClickPosition(this.touchLocation) + this.emit(InputEvent.DoubleClick, loc) + } } else { - this.tapTimeout = setTimeout(function () { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + this.tapTimeout = setTimeout(() => { clearTimeout(this.tapTimeout) }, 500) } @@ -85,18 +90,17 @@ export default class Input extends EventEmitter { this.container.addEventListener('dblclick', (e) => { const data = this._getNormalisedClickPosition(e) ;(data as unknown as Record).event = e + if (e.shiftKey) (data as unknown as Record).multiSelect = true this.emit(InputEvent.DoubleClick, data) }) this.container.addEventListener('pointermove', (e) => { const data = this._getNormalisedClickPosition(e) ;(data as unknown as Record).event = e - this.emit('pointer-move', data) this.emit(InputEvent.PointerMove, data) }) document.addEventListener('keyup', (e) => { - this.emit('key-up', e) this.emit(InputEvent.KeyUp, e) }) @@ -112,9 +116,16 @@ export default class Input extends EventEmitter { // }) } - _getNormalisedClickPosition(e) { + public on( + eventType: T, + listener: (arg: InputEventPayload[T]) => void + ): void { + super.on(eventType, listener) + } + + _getNormalisedClickPosition(e: MouseEvent | Touch) { // Reference: https://threejsfundamentals.org/threejs/lessons/threejs-picking.html - const canvas = this.container + const canvas = this.container as HTMLCanvasElement const rect = this.container.getBoundingClientRect() const pos = { diff --git a/packages/viewer/src/modules/loaders/GeometryConverter.ts b/packages/viewer/src/modules/loaders/GeometryConverter.ts index b0f938cac5..e8b77de02a 100644 --- a/packages/viewer/src/modules/loaders/GeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/GeometryConverter.ts @@ -1,5 +1,5 @@ -import { NodeData } from '../..' -import { GeometryData } from '../converter/Geometry' +import { type GeometryData } from '../converter/Geometry' +import type { NodeData } from '../tree/WorldTree' export enum SpeckleType { View3D = 'View3D', @@ -40,6 +40,6 @@ export const SpeckleTypeAllRenderables: SpeckleType[] = [ export abstract class GeometryConverter { public abstract getSpeckleType(node: NodeData): SpeckleType - public abstract convertNodeToGeometryData(node: NodeData): GeometryData + public abstract convertNodeToGeometryData(node: NodeData): GeometryData | null public abstract disposeNodeGeometryData(node: NodeData): void } diff --git a/packages/viewer/src/modules/loaders/Loader.ts b/packages/viewer/src/modules/loaders/Loader.ts index 469fd70443..10f5a429c3 100644 --- a/packages/viewer/src/modules/loaders/Loader.ts +++ b/packages/viewer/src/modules/loaders/Loader.ts @@ -1,26 +1,43 @@ import EventEmitter from '../EventEmitter' export enum LoaderEvent { - LoadComplete = 'load-complete', LoadProgress = 'load-progress', LoadCancelled = 'load-cancelled', LoadWarning = 'load-warning' } +export interface LoaderEventPayload { + [LoaderEvent.LoadProgress]: { progress: number; id: string } + [LoaderEvent.LoadCancelled]: string + [LoaderEvent.LoadWarning]: { message: string } +} + export abstract class Loader extends EventEmitter { protected _resource: string - protected _resourceData: string | ArrayBuffer + protected _resourceData: string | ArrayBuffer | undefined public abstract get resource(): string public abstract get finished(): boolean - protected constructor(resource: string, resourceData: string | ArrayBuffer) { + protected constructor( + resource: string, + resourceData?: string | ArrayBuffer | undefined + ) { super() this._resource = resource this._resourceData = resourceData } + public on( + eventType: T, + listener: (arg: LoaderEventPayload[T]) => void + ): void { + super.on(eventType, listener) + } + public abstract load(): Promise - public abstract cancel() - public abstract dispose() + public abstract cancel(): void + public dispose(): void { + super.dispose() + } } diff --git a/packages/viewer/src/modules/loaders/OBJ/ObjConverter.ts b/packages/viewer/src/modules/loaders/OBJ/ObjConverter.ts index 2682027f53..fbc0738c15 100644 --- a/packages/viewer/src/modules/loaders/OBJ/ObjConverter.ts +++ b/packages/viewer/src/modules/loaders/OBJ/ObjConverter.ts @@ -1,17 +1,18 @@ -import { Group, Mesh, Object3D } from 'three' -import { TreeNode, WorldTree } from '../../tree/WorldTree' -import { - ConverterNodeDelegate, - ConverterResultDelegate -} from '../Speckle/SpeckleConverter' +import { Mesh, Object3D } from 'three' +import { type TreeNode, WorldTree } from '../../tree/WorldTree' +import type { ConverterResultDelegate } from '../Speckle/SpeckleConverter' import Logger from 'js-logger' +export type ObjConverterNodeDelegate = + | ((object: Object3D, node: TreeNode) => Promise) + | null + export class ObjConverter { - private lastAsyncPause: number + private lastAsyncPause: number = 0 private tree: WorldTree private readonly NodeConverterMapping: { - [name: string]: ConverterNodeDelegate + [name: string]: ObjConverterNodeDelegate } = { Group: this.groupToNode.bind(this), Mesh: this.MeshToNode.bind(this) @@ -33,7 +34,7 @@ export class ObjConverter { objectURL: string, object: Object3D, callback: ConverterResultDelegate, - node: TreeNode = null + node: TreeNode | null = null ) { await this.asyncPause() @@ -68,14 +69,15 @@ export class ObjConverter { } } - private directNodeConverterExists(obj) { + private directNodeConverterExists(obj: Object3D) { return obj.type in this.NodeConverterMapping } - private async convertToNode(obj, node) { + private async convertToNode(obj: Object3D, node: TreeNode) { try { if (this.directNodeConverterExists(obj)) { - return await this.NodeConverterMapping[obj.type](obj, node) + const delegate = this.NodeConverterMapping[obj.type] + if (delegate) return await delegate(obj, node) } return null } catch (e) { @@ -84,7 +86,8 @@ export class ObjConverter { } } - private async MeshToNode(obj: Mesh, node) { + private async MeshToNode(_obj: Object3D, node: TreeNode) { + const obj = _obj as Mesh if (!obj) return if ( !obj.geometry.attributes.position || @@ -99,10 +102,12 @@ export class ObjConverter { node.model.raw.vertices = obj.geometry.attributes.position.array node.model.raw.faces = obj.geometry.index?.array node.model.raw.colors = obj.geometry.attributes.color?.array + return Promise.resolve() } - private groupToNode(obj: Group, node) { + private groupToNode(obj: Object3D, node: TreeNode) { obj node + return Promise.resolve() } } diff --git a/packages/viewer/src/modules/loaders/OBJ/ObjGeometryConverter.ts b/packages/viewer/src/modules/loaders/OBJ/ObjGeometryConverter.ts index d4355bf325..d7a77a17bc 100644 --- a/packages/viewer/src/modules/loaders/OBJ/ObjGeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/OBJ/ObjGeometryConverter.ts @@ -1,6 +1,6 @@ import { Matrix4 } from 'three' -import { NodeData } from '../../..' -import { GeometryData } from '../../converter/Geometry' +import { type NodeData } from '../../..' +import { type GeometryData } from '../../converter/Geometry' import { GeometryConverter, SpeckleType } from '../GeometryConverter' import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils' @@ -11,16 +11,20 @@ export class ObjGeometryConverter extends GeometryConverter { return SpeckleType.BlockInstance case 'Mesh': return SpeckleType.Mesh + default: + return SpeckleType.Unknown } } - public convertNodeToGeometryData(node: NodeData): GeometryData { + public convertNodeToGeometryData(node: NodeData): GeometryData | null { const type = this.getSpeckleType(node) switch (type) { case SpeckleType.BlockInstance: return this.BlockInstanceToGeometryData(node) case SpeckleType.Mesh: return this.MeshToGeometryData(node) + default: + return null } } @@ -53,8 +57,8 @@ export class ObjGeometryConverter extends GeometryConverter { /** * MESH */ - private MeshToGeometryData(node: NodeData): GeometryData { - if (!node.raw) return + private MeshToGeometryData(node: NodeData): GeometryData | null { + if (!node.raw) return null const conversionFactor = 1 if (!node.raw.geometry.index || node.raw.geometry.index.array.length === 0) { diff --git a/packages/viewer/src/modules/loaders/OBJ/ObjLoader.ts b/packages/viewer/src/modules/loaders/OBJ/ObjLoader.ts index e67c5207e7..9836c4b1b7 100644 --- a/packages/viewer/src/modules/loaders/OBJ/ObjLoader.ts +++ b/packages/viewer/src/modules/loaders/OBJ/ObjLoader.ts @@ -10,7 +10,7 @@ export class ObjLoader extends Loader { private baseLoader: OBJLoader private converter: ObjConverter private tree: WorldTree - private isFinished: boolean + private isFinished: boolean = false public get resource(): string { return this._resource @@ -66,12 +66,16 @@ export class ObjLoader extends Loader { pload.then(async () => { const t0 = performance.now() - const res = await this.tree - .getRenderTree(this._resource) - .buildRenderTree(new ObjGeometryConverter()) - Logger.log('Tree build time -> ', performance.now() - t0) - this.isFinished = true - resolve(res) + const renderTree = this.tree.getRenderTree(this._resource) + if (renderTree) { + const res = await renderTree.buildRenderTree(new ObjGeometryConverter()) + Logger.log('Tree build time -> ', performance.now() - t0) + this.isFinished = true + resolve(res) + } else { + Logger.error(`Could not get render tree for ${this._resource}`) + reject() + } }) pload.catch(() => { Logger.error(`Could not load ${this._resource}`) @@ -82,9 +86,9 @@ export class ObjLoader extends Loader { public cancel() { this.isFinished = false - throw new Error('Method not implemented.') } + public dispose() { - this.baseLoader = null + super.dispose() } } diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts index 250ceb8981..72dc43ae36 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleConverter.ts @@ -1,18 +1,22 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { MathUtils } from 'three' -import { TreeNode, WorldTree } from '../../tree/WorldTree' +import { type TreeNode, WorldTree } from '../../tree/WorldTree' import Logger from 'js-logger' import { NodeMap } from '../../tree/NodeMap' +import type { SpeckleObject } from '../../..' +import type ObjectLoader from '@speckle/objectloader' export type ConverterResultDelegate = () => Promise -export type ConverterNodeDelegate = (object, node) => Promise +export type SpeckleConverterNodeDelegate = + | ((object: SpeckleObject, node: TreeNode) => Promise) + | null /** * Utility class providing some top level conversion methods. * Warning: HIC SVNT DRACONES. */ export default class SpeckleConverter { - private objectLoader + private objectLoader: ObjectLoader private activePromises: number private maxChildrenPromises: number private spoofIDs = false @@ -21,7 +25,7 @@ export default class SpeckleConverter { private instanceCounter = 0 private readonly NodeConverterMapping: { - [name: string]: ConverterNodeDelegate + [name: string]: SpeckleConverterNodeDelegate } = { View3D: this.View3DToNode.bind(this), BlockInstance: this.BlockInstanceToNode.bind(this), @@ -45,7 +49,7 @@ export default class SpeckleConverter { private readonly IgnoreNodes = ['Parameter'] - constructor(objectLoader: unknown, tree: WorldTree) { + constructor(objectLoader: ObjectLoader, tree: WorldTree) { if (!objectLoader) { Logger.warn( 'Converter initialized without a corresponding object loader. Any objects that include references will throw errors.' @@ -67,9 +71,9 @@ export default class SpeckleConverter { */ public async traverse( objectURL: string, - obj, + obj: SpeckleObject, callback: ConverterResultDelegate, - node: TreeNode = null + node: TreeNode | null = null ) { // Exit on primitives (string, ints, bools, bigints, etc.) if (obj === null || typeof obj !== 'object') return @@ -126,7 +130,7 @@ export default class SpeckleConverter { // If we can convert it, we should invoke the respective conversion routine. if (this.directNodeConverterExists(obj)) { try { - await this.convertToNode(obj.data || obj, childNode) + await this.convertToNode(obj, childNode) await callback() return } catch (e) { @@ -139,7 +143,7 @@ export default class SpeckleConverter { } } - const target = obj + const target: SpeckleObject = obj // Check if the object has a display value of sorts // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -167,7 +171,9 @@ export default class SpeckleConverter { await callback() } catch (e) { Logger.warn( - `(Traversing) Failed to convert obj with id: ${obj.id} — ${e.message}` + `(Traversing) Failed to convert obj with id: ${obj.id} — ${ + (e as never)['message'] + }` ) } } else { @@ -192,7 +198,7 @@ export default class SpeckleConverter { const elements = this.getElementsValue(obj) if (elements) { childrenConversionPromisses.push( - this.traverse(objectURL, elements, callback, childNode) + this.traverse(objectURL, elements as SpeckleObject, callback, childNode) ) this.activePromises += childrenConversionPromisses.length await Promise.all(childrenConversionPromisses) @@ -214,9 +220,19 @@ export default class SpeckleConverter { if (typeof target[prop] !== 'object' || target[prop] === null) continue if (this.activePromises >= this.maxChildrenPromises) { - await this.traverse(objectURL, target[prop], callback, childNode) + await this.traverse( + objectURL, + target[prop] as SpeckleObject, + callback, + childNode + ) } else { - const childPromise = this.traverse(objectURL, target[prop], callback, childNode) + const childPromise = this.traverse( + objectURL, + target[prop] as SpeckleObject, + callback, + childNode + ) childrenConversionPromisses.push(childPromise) } } @@ -225,7 +241,7 @@ export default class SpeckleConverter { this.activePromises -= childrenConversionPromisses.length } - private getNodeId(obj) { + private getNodeId(obj: SpeckleObject): string { if (this.spoofIDs) return MathUtils.generateUUID() return obj.id } @@ -235,19 +251,22 @@ export default class SpeckleConverter { * @param {[type]} arr [description] * @return {[type]} [description] */ - private async dechunk(arr) { + private async dechunk(arr: Array<{ referencedId: string }>) { if (!arr || arr.length === 0) return arr // Handles pre-chunking objects, or arrs that have not been chunked if (!arr[0].referencedId) return arr - const chunked = [] + const chunked: unknown[] = [] for (const ref of arr) { - const real = await this.objectLoader.getObject(ref.referencedId) + const real: Record = await this.objectLoader.getObject( + ref.referencedId + ) chunked.push(real.data) // await this.asyncPause() } - const dechunked = [].concat(...chunked) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dechunked = [].concat(...(chunked as any)) return dechunked } @@ -257,9 +276,11 @@ export default class SpeckleConverter { * @param {[type]} obj [description] * @return {[type]} [description] */ - private async resolveReference(obj) { + private async resolveReference(obj: SpeckleObject): Promise { if (obj.referencedId) { - const resolvedObj = await this.objectLoader.getObject(obj.referencedId) + const resolvedObj = (await this.objectLoader.getObject( + obj.referencedId + )) as SpeckleObject // this.asyncPause() return resolvedObj } else return obj @@ -270,10 +291,8 @@ export default class SpeckleConverter { * @param {[type]} obj [description] * @return {[type]} [description] */ - private getSpeckleType(obj): string { - let rawType = 'Base' - if (obj.data) rawType = obj.data.speckle_type ? obj.data.speckle_type : 'Base' - else rawType = obj.speckle_type ? obj.speckle_type : 'Base' + private getSpeckleType(obj: SpeckleObject): string { + const rawType = obj.speckle_type ? obj.speckle_type : 'Base' const lookup = this.typeLookupTable[rawType] if (lookup) return lookup @@ -291,11 +310,9 @@ export default class SpeckleConverter { return typeRet } - private getSpeckleTypeChain(obj): string[] { + private getSpeckleTypeChain(obj: SpeckleObject): string[] { let type = ['Base'] - if (obj.data) - type = obj.data.speckle_type ? obj.data.speckle_type.split(':').reverse() : type - else type = obj.speckle_type ? obj.speckle_type.split(':').reverse() : type + type = obj.speckle_type ? obj.speckle_type.split(':').reverse() : type type = type.map((value: string) => { return value.split('.').reverse()[0] }) @@ -303,15 +320,16 @@ export default class SpeckleConverter { return type } - private directNodeConverterExists(obj) { + private directNodeConverterExists(obj: SpeckleObject) { return this.getSpeckleType(obj) in this.NodeConverterMapping } - private async convertToNode(obj, node) { + private async convertToNode(obj: SpeckleObject, node: TreeNode) { if (obj.referencedId) obj = await this.resolveReference(obj) try { if (this.directNodeConverterExists(obj)) { - return await this.NodeConverterMapping[this.getSpeckleType(obj)](obj, node) + const delegate = this.NodeConverterMapping[this.getSpeckleType(obj)] + if (delegate) return await delegate(obj, node) } return null } catch (e) { @@ -320,7 +338,7 @@ export default class SpeckleConverter { } } - private getDisplayValue(obj) { + private getDisplayValue(obj: SpeckleObject) { const displayValue = obj['displayValue'] || obj['@displayValue'] || @@ -340,11 +358,11 @@ export default class SpeckleConverter { return null } - private getElementsValue(obj) { + private getElementsValue(obj: SpeckleObject) { return obj['elements'] || obj['@elements'] } - private getBlockDefinition(obj) { + private getBlockDefinition(obj: SpeckleObject) { return ( obj['@blockDefinition'] || obj['blockDefinition'] || @@ -353,12 +371,12 @@ export default class SpeckleConverter { ) } - private getBlockDefinitionGeometry(obj) { + private getBlockDefinitionGeometry(obj: SpeckleObject) { return obj['@geometry'] || obj['geometry'] } /** We're wasting a few milis here, but it is what it is */ - private getCompoundId(baseId, counter) { + private getCompoundId(baseId: string, counter: number) { const index = baseId.indexOf(NodeMap.COMPOUND_ID_CHAR) if (index === -1) { return baseId + NodeMap.COMPOUND_ID_CHAR + counter @@ -375,8 +393,12 @@ export default class SpeckleConverter { * NODES */ - private async View3DToNode(obj, node) { + private async View3DToNode(obj: SpeckleObject, _node: TreeNode) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore obj.origin.units = obj.units + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore obj.target.units = obj.units } @@ -384,15 +406,19 @@ export default class SpeckleConverter { * It's only looking for 'elements' and 'displayValues' * I think it can be used for RevitInstances as well to replace it's current lookup, but I'm afraid to do it */ - private async displayableLookup(obj, node, instanced) { + private async displayableLookup( + obj: SpeckleObject, + node: TreeNode, + instanced: boolean + ) { if (this.directNodeConverterExists(obj)) { await this.convertToNode(obj, node) } else { const displayValues = this.getDisplayValue(obj) const elements = this.getElementsValue(obj) const entries = [ - ...(displayValues ? displayValues : []), - ...(elements ? elements : []) + ...(displayValues ? (displayValues as SpeckleObject[]) : []), + ...(elements ? (elements as SpeckleObject[]) : []) ] for (const entry of entries) { const value = await this.resolveReference(entry) @@ -411,16 +437,16 @@ export default class SpeckleConverter { } private async parseInstanceDefinitionGeometry( - instanceObj, - defGeometry, - instanceNode + instanceObj: SpeckleObject, + defGeometry: SpeckleObject, + instanceNode: TreeNode ) { const transformNodeId = MathUtils.generateUUID() let transformData = null /** Legacy form of Transform */ if (Array.isArray(instanceObj.transform)) { transformData = this.getEmptyTransformData(transformNodeId) - transformData.units = instanceObj.units + transformData.units = instanceObj.units as string transformData.matrix = instanceObj.transform } else { transformData = instanceObj.transform @@ -447,7 +473,11 @@ export default class SpeckleConverter { await this.displayableLookup(defGeometry, childNode, true) } - private async parseInstanceElement(instanceObj, elementObj, instanceNode) { + private async parseInstanceElement( + _instanceObj: SpeckleObject, + elementObj: SpeckleObject, + instanceNode: TreeNode + ) { const childNode: TreeNode = this.tree.parse({ id: this.getNodeId(elementObj), raw: elementObj, @@ -458,43 +488,49 @@ export default class SpeckleConverter { await this.displayableLookup(elementObj, childNode, false) } - private async BlockInstanceToNode(obj, node) { - const definition = await this.resolveReference(this.getBlockDefinition(obj)) + private async BlockInstanceToNode(obj: SpeckleObject, node: TreeNode) { + const definition: SpeckleObject = await this.resolveReference( + this.getBlockDefinition(obj) as SpeckleObject + ) node.model.raw.definition = definition - for (const def of this.getBlockDefinitionGeometry(definition)) { + for (const def of this.getBlockDefinitionGeometry(definition) as SpeckleObject[]) { const ref = await this.resolveReference(def) await this.parseInstanceDefinitionGeometry(obj, ref, node) } const elements = this.getElementsValue(obj) if (elements) { - for (const element of elements) { + for (const element of elements as SpeckleObject[]) { const elementObj = await this.resolveReference(element) this.parseInstanceElement(obj, elementObj, node) } } } - private async RevitInstanceToNode(obj, node) { - const definition = await this.resolveReference(obj.definition) + private async RevitInstanceToNode(obj: SpeckleObject, node: TreeNode) { + const definition = await this.resolveReference(obj.definition as SpeckleObject) node.model.raw.definition = definition await this.parseInstanceDefinitionGeometry(obj, definition, node) const elements = this.getElementsValue(obj) if (elements) { - for (const element of elements) { + for (const element of elements as SpeckleObject[]) { const elementObj = await this.resolveReference(element) this.parseInstanceElement(obj, elementObj, node) } } } - private async PointcloudToNode(obj, node) { + private async PointcloudToNode(obj: SpeckleObject, node: TreeNode) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.points = await this.dechunk(obj.points) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.colors = await this.dechunk(obj.colors) } - private async BrepToNode(obj, node) { + private async BrepToNode(obj: SpeckleObject, node: TreeNode) { try { if (!obj) return @@ -503,7 +539,7 @@ export default class SpeckleConverter { if (Array.isArray(displayValue)) displayValue = displayValue[0] //Just take the first display value for now (not ideal) if (!displayValue) return - const ref = await this.resolveReference(displayValue) + const ref = await this.resolveReference(displayValue as SpeckleObject) const nestedNode: TreeNode = this.tree.parse({ id: node.model.instanced ? this.getCompoundId(ref.id, this.instanceCounter++) @@ -531,32 +567,37 @@ export default class SpeckleConverter { } } - private async MeshToNode(obj, node) { + private async MeshToNode(obj: SpeckleObject, node: TreeNode) { if (!obj) return - if (!obj.vertices || obj.vertices.length === 0) { + if (!obj.vertices || (obj.vertices as Array).length === 0) { Logger.warn( `Object id ${obj.id} of type ${obj.speckle_type} has no vertex position data and will be ignored` ) return } - if (!obj.faces || obj.faces.length === 0) { + if (!obj.faces || (obj.faces as Array).length === 0) { Logger.warn( `Object id ${obj.id} of type ${obj.speckle_type} has no face data and will be ignored` ) return } - + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.vertices = await this.dechunk(obj.vertices) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.faces = await this.dechunk(obj.faces) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore node.model.raw.colors = await this.dechunk(obj.colors) } - private async TextToNode(obj, node) { + private async TextToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async DimensionToNode(obj, node) { - const displayValues = [...this.getDisplayValue(obj)] + private async DimensionToNode(obj: SpeckleObject, node: TreeNode) { + const displayValues = [...(this.getDisplayValue(obj) as SpeckleObject[])] for (const displayValue of displayValues) { const childNode: TreeNode = this.tree.parse({ id: this.getNodeId(displayValue), @@ -598,28 +639,36 @@ export default class SpeckleConverter { await this.convertToNode(textObj, textNode) } - private async PointToNode(obj, node) { + private async PointToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async LineToNode(obj, node) { + private async LineToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async PolylineToNode(obj, node) { - node.model.raw.value = await this.dechunk(obj.value) + private async PolylineToNode(obj: SpeckleObject, node: TreeNode) { + node.model.raw.value = await this.dechunk( + obj.value as Array<{ referencedId: string }> + ) } - private async BoxToNode(obj, node) { + private async BoxToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async PolycurveToNode(obj, node) { + private async PolycurveToNode(obj: SpeckleObject, node: TreeNode) { node.model.nestedNodes = [] - for (let i = 0; i < obj.segments.length; i++) { - let element = obj.segments[i] + for ( + let i = 0; + i < (obj as unknown as { segments: SpeckleObject[] }).segments.length; + i++ + ) { + let element = (obj as unknown as { segments: SpeckleObject[] }).segments[ + i + ] as SpeckleObject /** Not a big fan of this... */ if (!this.directNodeConverterExists(element)) { - element = this.getDisplayValue(element) + element = this.getDisplayValue(element) as SpeckleObject if (element.referencedId) { element = await this.resolveReference(element) } @@ -636,8 +685,8 @@ export default class SpeckleConverter { } } - private async CurveToNode(obj, node) { - let displayValue = this.getDisplayValue(obj) + private async CurveToNode(obj: SpeckleObject, node: TreeNode) { + let displayValue: SpeckleObject = this.getDisplayValue(obj) as SpeckleObject if (!displayValue) { Logger.warn( `Object ${obj.id} of type ${obj.speckle_type} has no display value and will be ignored` @@ -645,7 +694,7 @@ export default class SpeckleConverter { return } node.model.nestedNodes = [] - displayValue = await this.resolveReference(obj.displayValue) + displayValue = await this.resolveReference(obj.displayValue as SpeckleObject) displayValue.units = displayValue.units || obj.units const nestedNode: TreeNode = this.tree.parse({ id: this.getNodeId(displayValue), @@ -658,15 +707,15 @@ export default class SpeckleConverter { node.model.nestedNodes.push(nestedNode) } - private async CircleToNode(obj, node) { + private async CircleToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async ArcToNode(obj, node) { + private async ArcToNode(_obj: SpeckleObject, _node: TreeNode) { return } - private async EllipseToNode(obj, node) { + private async EllipseToNode(_obj: SpeckleObject, _node: TreeNode) { return } } diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts index af532ef525..82efd25ba0 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleGeometryConverter.ts @@ -1,7 +1,7 @@ -import { Geometry, GeometryData } from '../../converter/Geometry' +import { Geometry, type GeometryData } from '../../converter/Geometry' import MeshTriangulationHelper from '../../converter/MeshTriangulationHelper' import { getConversionFactor } from '../../converter/Units' -import { NodeData } from '../../tree/WorldTree' +import { type NodeData } from '../../tree/WorldTree' import { Box3, EllipseCurve, Matrix4, Vector2, Vector3 } from 'three' import Logger from 'js-logger' import { GeometryConverter, SpeckleType } from '../GeometryConverter' @@ -10,25 +10,22 @@ export class SpeckleGeometryConverter extends GeometryConverter { public typeLookupTable: { [type: string]: SpeckleType } = {} public getSpeckleType(node: NodeData): SpeckleType { - let rawType = 'Base' - if (node.raw.data) - rawType = node.raw.data.speckle_type ? node.raw.data.speckle_type : 'Base' - else rawType = node.raw.speckle_type ? node.raw.speckle_type : 'Base' + const rawType = node.raw.speckle_type ? node.raw.speckle_type : 'Base' const lookup = this.typeLookupTable[rawType] if (lookup) { return lookup } - let typeRet = SpeckleType.Unknown - let typeChain = [] + let typeRet: SpeckleType = SpeckleType.Unknown + let typeChain: string[] = [] typeChain = rawType.split(':').reverse() typeChain = typeChain.map((value: string) => { return value.split('.').reverse()[0] }) for (const type of typeChain) { if (type in SpeckleType) { - typeRet = type + typeRet = type as SpeckleType break } } @@ -36,7 +33,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { return typeRet } - public convertNodeToGeometryData(node: NodeData): GeometryData { + public convertNodeToGeometryData(node: NodeData): GeometryData | null { const type = this.getSpeckleType(node) switch (type) { case SpeckleType.BlockInstance: @@ -168,13 +165,13 @@ export class SpeckleGeometryConverter extends GeometryConverter { } /** BLOCK INSTANCE */ - private BlockInstanceToGeometryData(node: NodeData): GeometryData { + private BlockInstanceToGeometryData(node: NodeData): GeometryData | null { node return null } /** REVIT INSTANCE */ - private RevitInstanceToGeometryData(node: NodeData): GeometryData { + private RevitInstanceToGeometryData(node: NodeData): GeometryData | null { node return null } @@ -182,7 +179,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * POINT CLOUD */ - private PointcloudToGeometryData(node: NodeData) { + private PointcloudToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) const vertices = node.instanced ? node.raw.points.slice() : node.raw.points @@ -216,7 +213,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * BREP */ - private BrepToGeometryData(node) { + private BrepToGeometryData(node: NodeData): GeometryData | null { /** Breps don't (currently) have inherent geometryic description in the viewer. They are replaced * by their mesh display values */ @@ -227,14 +224,14 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * MESH */ - private MeshToGeometryData(node: NodeData): GeometryData { - if (!node.raw) return + private MeshToGeometryData(node: NodeData): GeometryData | null { + if (!node.raw) return null const conversionFactor = getConversionFactor(node.raw.units) const indices = [] - if (!node.raw.vertices) return - if (!node.raw.faces) return + if (!node.raw.vertices) return null + if (!node.raw.faces) return null const vertices = node.raw.vertices const faces = node.raw.faces @@ -293,7 +290,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * TEXT */ - private TextToGeometryData(node: NodeData): GeometryData { + private TextToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) const plane = node.raw.plane const position = new Vector3(plane.origin.x, plane.origin.y, plane.origin.z) @@ -316,11 +313,17 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * POINT */ - private PointToGeometryData(node: NodeData): GeometryData { + private PointToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) return { attributes: { - POSITION: this.PointToFloatArray(node.raw) + POSITION: this.PointToFloatArray( + node.raw as { value: Array; units: string } & { + x: number + y: number + z: number + } + ) }, bakeTransform: new Matrix4().makeScale( conversionFactor, @@ -334,7 +337,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * LINE */ - private LineToGeometryData(node: NodeData): GeometryData { + private LineToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) return { attributes: { @@ -354,7 +357,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * POLYLINE */ - private PolylineToGeometryData(node: NodeData): GeometryData { + private PolylineToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) if (node.raw.closed) @@ -375,7 +378,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * BOX */ - private BoxToGeometryData(node: NodeData) { + private BoxToGeometryData(node: NodeData): GeometryData | null { /** * Right, so we're cheating here a bit. We're using three's box geometry * to get the vertices and indices. Normally we could(should) do that by hand @@ -436,25 +439,30 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * POLYCURVE */ - private PolycurveToGeometryData(node: NodeData): GeometryData { + private PolycurveToGeometryData(node: NodeData): GeometryData | null { + if (!node.nestedNodes || node.nestedNodes.length === 0) { + return null + } const buffers = [] + for (let i = 0; i < node.nestedNodes.length; i++) { const element = node.nestedNodes[i].model const conv = this.convertNodeToGeometryData(element) buffers.push(conv) } - return Geometry.mergeGeometryData(buffers) + return Geometry.mergeGeometryData(buffers as GeometryData[]) } /** * CURVE */ - private CurveToGeometryData(node) { - if (node.nestedNodes.length === 0) { + private CurveToGeometryData(node: NodeData): GeometryData | null { + if (!node.nestedNodes || node.nestedNodes.length === 0) { return null } const polylineGeometry = this.PolylineToGeometryData(node.nestedNodes[0].model) + if (!polylineGeometry || !polylineGeometry.attributes) return null return { attributes: { POSITION: polylineGeometry.attributes.POSITION @@ -467,7 +475,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * CIRCLE */ - private CircleToGeometryData(node: NodeData) { + private CircleToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) const curveSegmentLength = 0.1 * conversionFactor const points = this.getCircularCurvePoints( @@ -489,7 +497,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * ARC */ - private ArcToGeometryData(node: NodeData) { + private ArcToGeometryData(node: NodeData): GeometryData | null { const origin = new Vector3( node.raw.plane.origin.x, node.raw.plane.origin.y, @@ -591,7 +599,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { /** * ELLIPSE */ - private EllipseToGeometryData(node: NodeData) { + private EllipseToGeometryData(node: NodeData): GeometryData | null { const conversionFactor = getConversionFactor(node.raw.units) const center = new Vector3( @@ -639,8 +647,24 @@ export class SpeckleGeometryConverter extends GeometryConverter { */ private getCircularCurvePoints( - plane, - radius, + plane: { + xdir: { value: Array; units: string } & { + x: number + y: number + z: number + } + ydir: { value: Array; units: string } & { + x: number + y: number + z: number + } + origin: { value: Array; units: string } & { + x: number + y: number + z: number + } + }, + radius: number, startAngle = 0, endAngle = 2 * Math.PI, res = 0.1 @@ -673,7 +697,10 @@ export class SpeckleGeometryConverter extends GeometryConverter { return points } - private PointToVector3(obj, scale = true) { + private PointToVector3( + obj: { value: Array; units: string } & { x: number; y: number; z: number }, + scale = true + ) { const conversionFactor = scale ? getConversionFactor(obj.units) : 1 let v = null if (obj.value) { @@ -694,7 +721,9 @@ export class SpeckleGeometryConverter extends GeometryConverter { return v } - private PointToFloatArray(obj) { + private PointToFloatArray( + obj: { value: Array; units: string } & { x: number; y: number; z: number } + ) { if (obj.value) { return [obj.value[0], obj.value[1], obj.value[2]] } else { @@ -704,7 +733,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { private FlattenVector3Array(input: Vector3[] | Vector2[]): number[] { const output = new Array(input.length * 3) - const vBuff = [] + const vBuff: Array = [] for (let k = 0, l = 0; k < input.length; k++, l += 3) { input[k].toArray(vBuff) output[l] = vBuff[0] @@ -733,7 +762,7 @@ export class SpeckleGeometryConverter extends GeometryConverter { return colors } - private srgbToLinear(x) { + private srgbToLinear(x: number) { if (x <= 0) return 0 else if (x >= 1) return 1 else if (x < 0.04045) return x / 12.92 diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts index d6a90ca496..85c9c7c76c 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts @@ -3,14 +3,13 @@ import SpeckleConverter from './SpeckleConverter' import { Loader, LoaderEvent } from '../Loader' import ObjectLoader from '@speckle/objectloader' import { SpeckleGeometryConverter } from './SpeckleGeometryConverter' -import { WorldTree } from '../../..' +import { WorldTree, type SpeckleObject } from '../../..' import { AsyncPause } from '../../World' export class SpeckleLoader extends Loader { private loader: ObjectLoader private converter: SpeckleConverter private tree: WorldTree - private priority: number = 1 private isCancelled = false private isFinished = false @@ -25,17 +24,15 @@ export class SpeckleLoader extends Loader { constructor( targetTree: WorldTree, resource: string, - authToken: string, + authToken?: string, enableCaching?: boolean, - resourceData?: string | ArrayBuffer, - priority: number = 1 + resourceData?: string | ArrayBuffer ) { super(resource, resourceData) this.tree = targetTree - this.priority = priority - let token = null + let token = undefined try { - token = authToken || localStorage.getItem('AuthToken') + token = authToken || (localStorage.getItem('AuthToken') as string | undefined) } catch (error) { // Accessing localStorage may throw when executing on sandboxed document, ignore. } @@ -90,15 +87,19 @@ export class SpeckleLoader extends Loader { return Promise.resolve(false) } if (first) { - firstObjectPromise = this.converter.traverse(this._resource, obj, async () => { - viewerLoads++ - pause.tick(100) - if (pause.needsWait) { - await pause.wait(16) + firstObjectPromise = this.converter.traverse( + this._resource, + obj as SpeckleObject, + async () => { + viewerLoads++ + pause.tick(100) + if (pause.needsWait) { + await pause.wait(16) + } } - }) + ) first = false - total = obj.totalChildrenCount + total = obj.totalChildrenCount as number } current++ this.emit(LoaderEvent.LoadProgress, { @@ -147,6 +148,7 @@ export class SpeckleLoader extends Loader { } dispose() { + super.dispose() this.loader.dispose() } } diff --git a/packages/viewer/src/modules/materials/Materials.ts b/packages/viewer/src/modules/materials/Materials.ts index a6a216975f..a3f6ded614 100644 --- a/packages/viewer/src/modules/materials/Materials.ts +++ b/packages/viewer/src/modules/materials/Materials.ts @@ -1,6 +1,6 @@ import { Color, DoubleSide, FrontSide, Material, Texture, Vector2 } from 'three' import { GeometryType } from '../batching/Batch' -import { TreeNode } from '../tree/WorldTree' +import { type TreeNode } from '../tree/WorldTree' import { NodeRenderView } from '../tree/NodeRenderView' import SpeckleLineMaterial from './SpeckleLineMaterial' import SpeckleStandardMaterial from './SpeckleStandardMaterial' @@ -13,7 +13,7 @@ import SpeckleGhostMaterial from './SpeckleGhostMaterial' import SpeckleTextMaterial from './SpeckleTextMaterial' import { SpeckleMaterial } from './SpeckleMaterial' import SpecklePointColouredMaterial from './SpecklePointColouredMaterial' -import { Asset, AssetType, MaterialOptions } from '../../IViewer' +import { type Asset, AssetType, type MaterialOptions } from '../../IViewer' const defaultGradient: Asset = { id: 'defaultGradient', @@ -44,6 +44,7 @@ export enum FilterMaterialType { HIDDEN } +/** TO DO: This still sucks */ export interface FilterMaterial { filterType: FilterMaterialType rampIndex?: number @@ -62,26 +63,26 @@ export default class Materials { public static readonly UNIFORM_VECTORS_USED = 33 public static readonly DEFAULT_ARTIFICIAL_ROUGHNESS = 0.6 /** The inverse of "shininess" */ private readonly materialMap: { [hash: number]: Material } = {} - private meshGhostMaterial: Material = null - private meshGradientMaterial: Material = null - private meshTransparentGradientMaterial: Material = null - private meshColoredMaterial: Material = null - private meshTransparentColoredMaterial: Material = null - private meshHiddenMaterial: Material = null + private meshGhostMaterial: Material + private meshGradientMaterial: Material + private meshTransparentGradientMaterial: Material + private meshColoredMaterial: Material + private meshTransparentColoredMaterial: Material + private meshHiddenMaterial: Material - private lineGhostMaterial: Material = null - private lineColoredMaterial: Material = null - private lineHiddenMaterial: Material = null + private lineGhostMaterial: Material + private lineColoredMaterial: Material + private lineHiddenMaterial: Material - private pointGhostMaterial: Material = null - private pointCloudColouredMaterial: Material = null - private pointCloudGradientMaterial: Material = null + private pointGhostMaterial: Material + private pointCloudColouredMaterial: Material + private pointCloudGradientMaterial: Material - private textGhostMaterial: Material = null - private textColoredMaterial: Material = null - private textHiddenMaterial: Material = null + private textGhostMaterial: Material + private textColoredMaterial: Material + private textHiddenMaterial: Material - private defaultGradientTextureData: ImageData = null + private defaultGradientTextureData!: ImageData private static readonly NullRenderMaterialHash = this.hashCode( GeometryType.MESH.toString() @@ -112,11 +113,11 @@ export default class Materials { ) public static renderMaterialFromNode( - materialNode: TreeNode, - geometryNode: TreeNode - ): RenderMaterial { + materialNode: TreeNode | null, + geometryNode: TreeNode | null + ): RenderMaterial | null { if (!materialNode) return null - let renderMaterial: RenderMaterial = null + let renderMaterial: RenderMaterial | null = null if (materialNode.model.raw.renderMaterial) { renderMaterial = { id: materialNode.model.raw.renderMaterial.id, @@ -128,15 +129,17 @@ export default class Materials { roughness: materialNode.model.raw.renderMaterial.roughness, metalness: materialNode.model.raw.renderMaterial.metalness, vertexColors: - geometryNode.model.raw.colors && geometryNode.model.raw.colors.length > 0 + geometryNode && + geometryNode.model.raw.colors && + geometryNode.model.raw.colors.length > 0 } } return renderMaterial } - public static displayStyleFromNode(node: TreeNode): DisplayStyle { + public static displayStyleFromNode(node: TreeNode | null): DisplayStyle | null { if (!node) return null - let displayStyle: DisplayStyle = null + let displayStyle: DisplayStyle | null = null if (node.model.raw.displayStyle) { /** If there are no units specified, we ignore the line width value */ let lineWeight = node.model.raw.displayStyle.lineweight || 0 @@ -189,21 +192,32 @@ export default class Materials { return plm } - private static hashCode(s: string) { - let h + private static hashCode(s: string): number { + let h = 0 for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0 return h } - public static isMaterialInstance(material): material is Material { + public static isMaterialInstance( + material: Material | FilterMaterial | RenderMaterial | DisplayStyle + ): material is Material { return material instanceof Material } - public static isFilterMaterial(material): material is FilterMaterial { + public static isFilterMaterial( + material: Material | FilterMaterial | RenderMaterial | DisplayStyle + ): material is FilterMaterial { return 'filterType' in material } - public static isRendeMaterial(materialData): materialData is RenderMaterial { + public static isRendeMaterial( + materialData: + | Material + | FilterMaterial + | RenderMaterial + | DisplayStyle + | MaterialOptions + ): materialData is RenderMaterial { return ( 'color' in materialData && 'opacity' in materialData && @@ -213,14 +227,21 @@ export default class Materials { ) } - public static isDisplayStyle(materialData): materialData is DisplayStyle { + public static isDisplayStyle( + materialData: + | Material + | FilterMaterial + | RenderMaterial + | DisplayStyle + | MaterialOptions + ): materialData is DisplayStyle { return 'color' in materialData && 'lineWeight' in materialData } public static getMaterialHash( renderView: NodeRenderView, - materialData?: RenderMaterial | DisplayStyle | MaterialOptions - ) { + materialData?: RenderMaterial | DisplayStyle | MaterialOptions | null + ): number { if (!materialData) { materialData = renderView.renderData.renderMaterial || renderView.renderData.displayStyle @@ -733,7 +754,7 @@ export default class Materials { public getMaterial( hash: number, - material: RenderMaterial | DisplayStyle, + material: RenderMaterial | DisplayStyle | null, type: GeometryType ): Material { let mat @@ -742,7 +763,7 @@ export default class Materials { mat = this.getMeshMaterial(hash, material as RenderMaterial) break case GeometryType.LINE: - mat = this.getLineMaterial(hash, material) + mat = this.getLineMaterial(hash, material as DisplayStyle) break case GeometryType.POINT: mat = this.getPointMaterial(hash, material as RenderMaterial) @@ -751,7 +772,7 @@ export default class Materials { mat = this.getPointCloudMaterial(hash, material as RenderMaterial) break case GeometryType.TEXT: - mat = this.getTextMaterial(hash, material) + mat = this.getTextMaterial(hash, material as DisplayStyle) break } // } @@ -801,7 +822,7 @@ export default class Materials { public getGhostMaterial( renderView: NodeRenderView, filterMaterial?: FilterMaterial - ): Material { + ): Material | null { filterMaterial switch (renderView.geometryType) { case GeometryType.MESH: @@ -820,7 +841,7 @@ export default class Materials { public getGradientMaterial( renderView: NodeRenderView, filterMaterial?: FilterMaterial - ): Material { + ): Material | null { switch (renderView.geometryType) { case GeometryType.MESH: { const material = renderView.transparent @@ -858,7 +879,7 @@ export default class Materials { public getColoredMaterial( renderView: NodeRenderView, filterMaterial?: FilterMaterial - ): Material { + ): Material | null { switch (renderView.geometryType) { case GeometryType.MESH: { const material = renderView.transparent @@ -896,7 +917,7 @@ export default class Materials { public getHiddenMaterial( renderView: NodeRenderView, filterMaterial?: FilterMaterial - ): Material { + ): Material | null { filterMaterial switch (renderView.geometryType) { case GeometryType.MESH: @@ -915,8 +936,8 @@ export default class Materials { public getFilterMaterial( renderView: NodeRenderView, filterMaterial: FilterMaterial - ): Material { - let retMaterial: Material + ): Material | null { + let retMaterial: Material | null = null switch (filterMaterial.filterType) { case FilterMaterialType.GHOST: retMaterial = this.getGhostMaterial(renderView, filterMaterial) @@ -934,7 +955,7 @@ export default class Materials { /** There's a bug in three.js where it checks for the length of the planes without checking if they exist first * It's been allegedly fixed in a later version but until we update we'll just assing an empty array */ - retMaterial.clippingPlanes = [] + if (retMaterial) retMaterial.clippingPlanes = [] return retMaterial } @@ -948,7 +969,7 @@ export default class Materials { public getFilterMaterialOptions( filterMaterial: FilterMaterial - ): FilterMaterialOptions { + ): FilterMaterialOptions | null { switch (filterMaterial.filterType) { case FilterMaterialType.COLORED: return { @@ -973,7 +994,8 @@ export default class Materials { rampIndexColor: filterMaterial.rampIndexColor !== undefined ? filterMaterial.rampIndexColor - : new Color() + : filterMaterial.rampIndex + ? new Color() .setRGB( this.defaultGradientTextureData.data[ Math.floor( @@ -998,7 +1020,8 @@ export default class Materials { 2 ] / 255 ) - .convertSRGBToLinear(), + .convertSRGBToLinear() + : undefined, rampTexture: filterMaterial.rampTexture ? filterMaterial.rampTexture : this.meshGradientMaterial.userData.gradientRamp.value, @@ -1006,6 +1029,8 @@ export default class Materials { ? filterMaterial.rampTexture.image.width : this.meshGradientMaterial.userData.gradientRamp.value.image.width } + default: + return null } } diff --git a/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts b/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts index 725618ace0..22576e17aa 100644 --- a/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts @@ -1,18 +1,28 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleBasicVert } from './shaders/speckle-basic-vert' import { speckleBasicFrag } from './shaders/speckle-basic-frag' -import { ShaderLib, Vector3, Material, IUniform, Vector2 } from 'three' +import { + ShaderLib, + Vector3, + Material, + type IUniform, + Vector2, + type MeshBasicMaterialParameters, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' import { Matrix4 } from 'three' -import { Geometry } from '../converter/Geometry' -import { ExtendedMeshBasicMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshBasicMaterial, type Uniforms } from './SpeckleMaterial' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { protected static readonly matBuff: Matrix4 = new Matrix4() protected static readonly vecBuff: Vector2 = new Vector2() - private _billboardPixelHeight: number + private _billboardPixelHeight!: number protected get vertexProgram(): string { return speckleBasicVert @@ -43,7 +53,7 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { this._billboardPixelHeight = value } - constructor(parameters, defines = []) { + constructor(parameters: MeshBasicMaterialParameters, defines: string[] = []) { super(parameters) this.init(defines) } @@ -53,7 +63,7 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -69,8 +79,14 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { } /** Called by three.js render loop */ - public onBeforeRender(_this, scene, camera, geometry, object, group) { - if (this.defines['BILLBOARD_FIXED']) { + public onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { + if (this.defines && this.defines['BILLBOARD_FIXED']) { const resolution = _this.getDrawingBufferSize(SpeckleBasicMaterial.vecBuff) SpeckleBasicMaterial.vecBuff.set( (this._billboardPixelHeight / resolution.x) * 2, @@ -81,7 +97,7 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { this.userData.invProjection.value.copy(SpeckleBasicMaterial.matBuff) } - if (this.defines['USE_RTE']) { + if (this.defines && this.defines['USE_RTE']) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpeckleDepthMaterial.ts b/packages/viewer/src/modules/materials/SpeckleDepthMaterial.ts index be23afb64b..50cbe81982 100644 --- a/packages/viewer/src/modules/materials/SpeckleDepthMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleDepthMaterial.ts @@ -1,17 +1,21 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleDepthVert } from './shaders/speckle-depth-vert' import { speckleDepthFrag } from './shaders/speckle-depth-frag' -import { ShaderLib, Vector3, IUniform } from 'three' +import { + BufferGeometry, + Camera, + Object3D, + Scene, + ShaderLib, + Vector3, + type IUniform, + type MeshDepthMaterialParameters +} from 'three' import { Matrix4, Material } from 'three' -import { ExtendedMeshDepthMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshDepthMaterial, type Uniforms } from './SpeckleMaterial' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleDepthMaterial extends ExtendedMeshDepthMaterial { - private static readonly matBuff: Matrix4 = new Matrix4() - private static readonly vecBuff0: Vector3 = new Vector3() - private static readonly vecBuff1: Vector3 = new Vector3() - private static readonly vecBuff2: Vector3 = new Vector3() - protected get vertexProgram(): string { return speckleDepthVert } @@ -37,7 +41,7 @@ class SpeckleDepthMaterial extends ExtendedMeshDepthMaterial { } } - constructor(parameters, defines = []) { + constructor(parameters: MeshDepthMaterialParameters, defines: string[] = []) { super(parameters) this.init(defines) } @@ -47,7 +51,7 @@ class SpeckleDepthMaterial extends ExtendedMeshDepthMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -62,8 +66,14 @@ class SpeckleDepthMaterial extends ExtendedMeshDepthMaterial { /** Another note here, this will NOT get called by three when rendering shadowmaps. We update the uniforms manually * inside SpeckleRenderer for shadowmaps */ - onBeforeRender(_this, scene, camera, geometry, object, group) { - if (this.defines['USE_RTE']) { + onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { + if (this.defines && this.defines['USE_RTE']) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpeckleDisplaceMaterial.ts b/packages/viewer/src/modules/materials/SpeckleDisplaceMaterial.ts index 933baf12ae..e8da58247d 100644 --- a/packages/viewer/src/modules/materials/SpeckleDisplaceMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleDisplaceMaterial.ts @@ -1,8 +1,8 @@ import { speckleDisplaceVert } from './shaders/speckle-displace.vert' import { speckleDisplaceFrag } from './shaders/speckle-displace-frag' -import { Material, Vector2 } from 'three' +import { Material, Vector2, type MeshBasicMaterialParameters } from 'three' import SpeckleBasicMaterial from './SpeckleBasicMaterial' -import { Uniforms } from './SpeckleMaterial' +import { type Uniforms } from './SpeckleMaterial' class SpeckleDisplaceMaterial extends SpeckleBasicMaterial { protected get vertexProgram(): string { @@ -17,7 +17,7 @@ class SpeckleDisplaceMaterial extends SpeckleBasicMaterial { return { ...super.uniformsDef, size: new Vector2(), displacement: 0 } } - constructor(parameters, defines) { + constructor(parameters: MeshBasicMaterialParameters, defines: string[] = []) { super(parameters, defines) } diff --git a/packages/viewer/src/modules/materials/SpeckleGhostMaterial.ts b/packages/viewer/src/modules/materials/SpeckleGhostMaterial.ts index 1ea55edf04..f7adb27a3e 100644 --- a/packages/viewer/src/modules/materials/SpeckleGhostMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleGhostMaterial.ts @@ -1,6 +1,7 @@ import { speckleGhostVert } from './shaders/speckle-ghost-vert' import { speckleGhostFrag } from './shaders/speckle-ghost-frag' import SpeckleBasicMaterial from './SpeckleBasicMaterial' +import type { MeshBasicMaterialParameters } from 'three' class SpeckleGhostMaterial extends SpeckleBasicMaterial { protected get vertexProgram(): string { @@ -11,7 +12,10 @@ class SpeckleGhostMaterial extends SpeckleBasicMaterial { return speckleGhostFrag } - constructor(parameters, defines = ['USE_RTE']) { + constructor( + parameters: MeshBasicMaterialParameters, + defines: string[] = ['USE_RTE'] + ) { super(parameters, defines) } } diff --git a/packages/viewer/src/modules/materials/SpeckleLineMaterial.ts b/packages/viewer/src/modules/materials/SpeckleLineMaterial.ts index 05a67c9adc..ed2d5259a3 100644 --- a/packages/viewer/src/modules/materials/SpeckleLineMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleLineMaterial.ts @@ -1,10 +1,22 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleLineVert } from './shaders/speckle-line-vert' import { speckleLineFrag } from './shaders/speckle-line-frag' -import { ShaderLib, Vector3, IUniform, Material } from 'three' -import { ExtendedLineMaterial, Uniforms } from './SpeckleMaterial' -import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' +import { + ShaderLib, + Vector3, + type IUniform, + Material, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' +import { ExtendedLineMaterial, type Uniforms } from './SpeckleMaterial' +import { + LineMaterial, + type LineMaterialParameters +} from 'three/examples/jsm/lines/LineMaterial.js' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleLineMaterial extends ExtendedLineMaterial { protected get vertexProgram(): string { @@ -32,7 +44,7 @@ class SpeckleLineMaterial extends ExtendedLineMaterial { this.needsUpdate = true } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: LineMaterialParameters, defines = ['USE_RTE']) { super(parameters) this.init(defines) } @@ -42,7 +54,7 @@ class SpeckleLineMaterial extends ExtendedLineMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -56,7 +68,13 @@ class SpeckleLineMaterial extends ExtendedLineMaterial { to.userData.pixelThreshold.value = from.userData.pixelThreshold.value } - onBeforeRender(_this, scene, camera, geometry, object, group) { + onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpeckleMaterial.ts b/packages/viewer/src/modules/materials/SpeckleMaterial.ts index 1fa742294b..3ffb70356a 100644 --- a/packages/viewer/src/modules/materials/SpeckleMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleMaterial.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ -/* eslint-disable camelcase */ import { AlwaysStencilFunc, - IUniform, + type IUniform, Material, MeshBasicMaterial, MeshDepthMaterial, @@ -10,24 +9,43 @@ import { MeshStandardMaterial, PointsMaterial, ReplaceStencilOp, - UniformsUtils + UniformsUtils, + type Shader } from 'three' import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' import { StencilOutlineType } from '../../IViewer' -import { MaterialOptions } from './MaterialOptions' +import { type MaterialOptions } from './MaterialOptions' class SpeckleUserData { + [k: string]: unknown toJSON() { return {} } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Uniforms = Record export class SpeckleMaterial { - protected _internalUniforms - protected _stencilOutline - public needsCopy: boolean + protected _internalUniforms!: Shader + protected _stencilOutline: StencilOutlineType = StencilOutlineType.NONE + public needsCopy: boolean = false + + protected get speckleUserData(): SpeckleUserData { + return (this as unknown as Material).userData + } + + protected set speckleUserData(value: SpeckleUserData) { + ;(this as unknown as Material).userData = value + } + + protected get speckleDefines(): Record | undefined { + return (this as unknown as Material).defines + } + + protected set speckleDefines(value: Record | undefined) { + ;(this as unknown as Material).defines = value + } protected get vertexProgram(): string { return '' @@ -50,55 +68,62 @@ export class SpeckleMaterial { } protected set stencilOutline(value: StencilOutlineType) { - this['stencilWrite'] = value === StencilOutlineType.NONE ? false : true - if (this['stencilWrite']) { - this['stencilWriteMask'] = 0xff - this['stencilRef'] = 0x00 - this['stencilFunc'] = AlwaysStencilFunc - this['stencilZFail'] = ReplaceStencilOp - this['stencilZPass'] = ReplaceStencilOp - this['stencilFail'] = ReplaceStencilOp + this._stencilOutline = value + const thisAsMaterial: Material = this as unknown as Material + thisAsMaterial.stencilWrite = value === StencilOutlineType.NONE ? false : true + if (thisAsMaterial.stencilWrite) { + thisAsMaterial.stencilWriteMask = 0xff + thisAsMaterial.stencilRef = 0x00 + thisAsMaterial.stencilFunc = AlwaysStencilFunc + thisAsMaterial.stencilZFail = ReplaceStencilOp + thisAsMaterial.stencilZPass = ReplaceStencilOp + thisAsMaterial.stencilFail = ReplaceStencilOp if (value === StencilOutlineType.OUTLINE_ONLY) { - this['colorWrite'] = false - this['depthWrite'] = false - this['stencilWrite'] = true + thisAsMaterial.colorWrite = false + thisAsMaterial.depthWrite = false + thisAsMaterial.stencilWrite = true } } } protected set pointSize(value: number) { - this['size'] = value + ;(this as unknown as PointsMaterial).size = value } - protected init(defines = []) { - this['userData'] = new SpeckleUserData() + protected init(defines: Array = []) { + this.speckleUserData = new SpeckleUserData() this.setUniforms(this.uniformsDef) this.setDefines(defines) - this['onBeforeCompile'] = this.onCompile + ;(this as unknown as Material)['onBeforeCompile'] = this.onCompile } protected setUniforms(def: Uniforms) { for (const k in def) { - this['userData'][k] = { + this.speckleUserData[k] = { value: def[k] } } - this['uniforms'] = UniformsUtils.merge([this.baseUniforms, this['userData']]) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(this as any)['uniforms'] = UniformsUtils.merge([ + this.baseUniforms, + this.speckleUserData + ]) } - protected setDefines(defines = []) { + protected setDefines(defines: Array = []) { if (defines) { - this['defines'] = {} + this.speckleDefines = {} for (let k = 0; k < defines.length; k++) { - this['defines'][defines[k]] = ' ' + this.speckleDefines[defines[k]] = ' ' } } } protected copyUniforms(material: Material) { for (const k in material.userData) { - if (this['userData'][k] !== undefined) - this['userData'][k].value = material.userData[k].value + if (this.speckleUserData[k] !== undefined) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.speckleUserData[k] as any).value = material.userData[k].value } } @@ -106,21 +131,23 @@ export class SpeckleMaterial { if (!this._internalUniforms) return for (const k in this.uniformsDef) { - this._internalUniforms.uniforms[k] = this['userData'][k] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this._internalUniforms.uniforms[k] = this.speckleUserData[k] as IUniform } } - protected copyFrom(source) { - this['userData'] = new SpeckleUserData() + protected copyFrom(source: Material) { + this.speckleUserData = new SpeckleUserData() this.setUniforms(this.uniformsDef) this.copyUniforms(source) this.bindUniforms() - Object.assign(this['defines'], source.defines) - if (source.needsCopy) this.needsCopy = source.needsCopy + Object.assign(this.speckleDefines as object, source.defines) + if ((source as unknown as SpeckleMaterial).needsCopy) + this.needsCopy = (source as unknown as SpeckleMaterial).needsCopy } // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected onCompile(shader, renderer) { + protected onCompile(shader: Shader) { this._internalUniforms = shader this.bindUniforms() shader.vertexShader = this.vertexProgram @@ -141,7 +168,7 @@ export class SpeckleMaterial { to.clippingPlanes = from.clippingPlanes to.clipShadows = from.clipShadows to.colorWrite = from.colorWrite - Object.assign(to.defines, from.defines) + Object.assign(to.defines as object, from.defines) to.depthFunc = from.depthFunc to.depthTest = from.depthTest to.depthWrite = from.depthWrite @@ -182,7 +209,6 @@ export class ExtendedMeshNormalMaterial extends MeshNormalMaterial {} export class ExtendedLineMaterial extends LineMaterial {} export class ExtendedPointsMaterial extends PointsMaterial {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ExtendedMeshStandardMaterial extends SpeckleMaterial {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ExtendedMeshBasicMaterial extends SpeckleMaterial {} diff --git a/packages/viewer/src/modules/materials/SpeckleNormalMaterial.ts b/packages/viewer/src/modules/materials/SpeckleNormalMaterial.ts index 76f5c86111..94e302ee7d 100644 --- a/packages/viewer/src/modules/materials/SpeckleNormalMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleNormalMaterial.ts @@ -1,11 +1,20 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleNormalVert } from './shaders/speckle-normal-vert' import { speckleNormalFrag } from './shaders/speckle-normal-frag' -import { ShaderLib, Vector3, IUniform } from 'three' +import { + BufferGeometry, + Camera, + Material, + Object3D, + Scene, + ShaderLib, + Vector3, + type IUniform, + type MeshNormalMaterialParameters +} from 'three' import { Matrix4 } from 'three' -import { Geometry } from '../converter/Geometry' -import { ExtendedMeshNormalMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshNormalMaterial, type Uniforms } from './SpeckleMaterial' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleNormalMaterial extends ExtendedMeshNormalMaterial { protected get vertexProgram(): string { @@ -29,7 +38,7 @@ class SpeckleNormalMaterial extends ExtendedMeshNormalMaterial { } } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: MeshNormalMaterialParameters, defines = ['USE_RTE']) { super(parameters) this.init(defines) } @@ -39,14 +48,20 @@ class SpeckleNormalMaterial extends ExtendedMeshNormalMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this } /** Called by three.js render loop */ - public onBeforeRender(_this, scene, camera, geometry, object, group) { + public onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpecklePointColouredMaterial.ts b/packages/viewer/src/modules/materials/SpecklePointColouredMaterial.ts index f0213a88ad..ba9d21f191 100644 --- a/packages/viewer/src/modules/materials/SpecklePointColouredMaterial.ts +++ b/packages/viewer/src/modules/materials/SpecklePointColouredMaterial.ts @@ -1,67 +1,28 @@ /* eslint-disable camelcase */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { specklePointVert } from './shaders/speckle-point-vert' import { specklePointFrag } from './shaders/speckle-point-frag' -import { - Matrix4, - NearestFilter, - PointsMaterial, - ShaderLib, - Texture, - UniformsUtils, - Vector3 -} from 'three' -import { Geometry } from '../converter/Geometry' +import { Material, NearestFilter, Texture, type PointsMaterialParameters } from 'three' +import type { Uniforms } from './SpeckleMaterial' +import SpecklePointMaterial from './SpecklePointMaterial' -class SpecklePointMaterial extends PointsMaterial { - private static readonly matBuff: Matrix4 = new Matrix4() - private static readonly vecBuff0: Vector3 = new Vector3() - private static readonly vecBuff1: Vector3 = new Vector3() - private static readonly vecBuff2: Vector3 = new Vector3() +class SpecklePointColouredMaterial extends SpecklePointMaterial { + protected get vertexProgram(): string { + return specklePointVert + } - constructor(parameters, defines = []) { - super(parameters) - this.userData.uViewer_high = { - value: new Vector3() - } - this.userData.uViewer_low = { - value: new Vector3() - } - this.userData.gradientRamp = { - value: null - } - ;(this as any).vertProgram = specklePointVert - ;(this as any).fragProgram = specklePointFrag - ;(this as any).uniforms = UniformsUtils.merge([ - ShaderLib.standard.uniforms, - { - uViewer_high: { - value: this.userData.uViewer_high.value - }, - uViewer_low: { - value: this.userData.uViewer_low.value - }, - gradientRamp: { - value: this.userData.gradientRamp.value - } - } - ]) + protected get fragmentProgram(): string { + return specklePointFrag + } - this.onBeforeCompile = function (shader) { - shader.uniforms.uViewer_high = this.userData.uViewer_high - shader.uniforms.uViewer_low = this.userData.uViewer_low - shader.uniforms.gradientRamp = this.userData.gradientRamp - shader.vertexShader = this.vertProgram - shader.fragmentShader = this.fragProgram - } + protected get uniformsDef(): Uniforms { + return { ...super.uniformsDef, gradientRamp: null } + } - if (defines) { - this.defines = { USE_GRADIENT_RAMP: '' } - } - for (let k = 0; k < defines.length; k++) { - this.defines[defines[k]] = ' ' - } + constructor( + parameters: PointsMaterialParameters, + defines: string[] = ['USE_RTE', 'USE_GRADIENT_RAMP'] + ) { + super(parameters, defines) } public setGradientTexture(texture: Texture) { @@ -72,50 +33,10 @@ class SpecklePointMaterial extends PointsMaterial { this.needsUpdate = true } - copy(source) { - super.copy(source) - this.userData = {} - this.userData.uViewer_high = { - value: new Vector3() - } - this.userData.uViewer_low = { - value: new Vector3() - } - this.userData.gradientRamp = { - value: null - } - - this.defines['USE_RTE'] = ' ' - this.defines['USE_GRADIENT_RAMP'] = ' ' - - return this - } - - onBeforeRender(_this, scene, camera, geometry, object, group) { - SpecklePointMaterial.matBuff.copy(camera.matrixWorldInverse) - SpecklePointMaterial.matBuff.elements[12] = 0 - SpecklePointMaterial.matBuff.elements[13] = 0 - SpecklePointMaterial.matBuff.elements[14] = 0 - SpecklePointMaterial.matBuff.multiply(object.matrixWorld) - object.modelViewMatrix.copy(SpecklePointMaterial.matBuff) - - SpecklePointMaterial.vecBuff0.set( - camera.matrixWorld.elements[12], - camera.matrixWorld.elements[13], - camera.matrixWorld.elements[14] - ) - - Geometry.DoubleToHighLowVector( - SpecklePointMaterial.vecBuff0, - SpecklePointMaterial.vecBuff1, - SpecklePointMaterial.vecBuff2 - ) - - this.userData.uViewer_low.value.copy(SpecklePointMaterial.vecBuff1) - this.userData.uViewer_high.value.copy(SpecklePointMaterial.vecBuff2) - - this.needsUpdate = true + public fastCopy(from: Material, to: Material) { + super.fastCopy(from, to) + to.userData.gradientRamp.value = from.userData.gradientRamp.value } } -export default SpecklePointMaterial +export default SpecklePointColouredMaterial diff --git a/packages/viewer/src/modules/materials/SpecklePointMaterial.ts b/packages/viewer/src/modules/materials/SpecklePointMaterial.ts index 50168cb34c..dbb577cd9f 100644 --- a/packages/viewer/src/modules/materials/SpecklePointMaterial.ts +++ b/packages/viewer/src/modules/materials/SpecklePointMaterial.ts @@ -1,10 +1,20 @@ /* eslint-disable camelcase */ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { specklePointVert } from './shaders/speckle-point-vert' import { specklePointFrag } from './shaders/speckle-point-frag' -import { IUniform, Material, Matrix4, PointsMaterial, ShaderLib, Vector3 } from 'three' -import { Geometry } from '../converter/Geometry' -import { ExtendedPointsMaterial, Uniforms } from './SpeckleMaterial' +import { + type IUniform, + Material, + PointsMaterial, + ShaderLib, + Vector3, + type PointsMaterialParameters, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' +import { ExtendedPointsMaterial, type Uniforms } from './SpeckleMaterial' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpecklePointMaterial extends ExtendedPointsMaterial { protected get vertexProgram(): string { @@ -26,7 +36,7 @@ class SpecklePointMaterial extends ExtendedPointsMaterial { } } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: PointsMaterialParameters, defines = ['USE_RTE']) { super(parameters) this.init(defines) } @@ -36,7 +46,7 @@ class SpecklePointMaterial extends ExtendedPointsMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -51,7 +61,13 @@ class SpecklePointMaterial extends ExtendedPointsMaterial { toStandard.sizeAttenuation = fromStandard.sizeAttenuation } - onBeforeRender(_this, scene, camera, geometry, object, group) { + onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) this.userData.uViewer_high.value.copy(_this.RTEBuffers.viewerHigh) diff --git a/packages/viewer/src/modules/materials/SpeckleShadowcatcherMaterial.ts b/packages/viewer/src/modules/materials/SpeckleShadowcatcherMaterial.ts index b3a682bd15..cdb7dacbd6 100644 --- a/packages/viewer/src/modules/materials/SpeckleShadowcatcherMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleShadowcatcherMaterial.ts @@ -1,8 +1,8 @@ import { speckleShadowcatcherVert } from './shaders/speckle-shadowcatcher-vert' import { speckleShadowcatcherFrag } from './shaders/speckle-shadowcatche-frag' import SpeckleBasicMaterial from './SpeckleBasicMaterial' -import { Vector4 } from 'three' -import { Uniforms } from './SpeckleMaterial' +import { Vector4, type MeshBasicMaterialParameters } from 'three' +import { type Uniforms } from './SpeckleMaterial' class SpeckleShadowcatcherMaterial extends SpeckleBasicMaterial { protected get vertexProgram(): string { @@ -26,7 +26,7 @@ class SpeckleShadowcatcherMaterial extends SpeckleBasicMaterial { } } - constructor(parameters, defines = []) { + constructor(parameters: MeshBasicMaterialParameters, defines = []) { super(parameters, defines) } } diff --git a/packages/viewer/src/modules/materials/SpeckleStandardColoredMaterial.ts b/packages/viewer/src/modules/materials/SpeckleStandardColoredMaterial.ts index b79cdcfff8..dad53a1b0a 100644 --- a/packages/viewer/src/modules/materials/SpeckleStandardColoredMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleStandardColoredMaterial.ts @@ -1,8 +1,8 @@ import { speckleStandardColoredVert } from './shaders/speckle-standard-colored-vert' import { speckleStandardColoredFrag } from './shaders/speckle-standard-colored-frag' -import { Texture, NearestFilter } from 'three' +import { Texture, NearestFilter, type MeshStandardMaterialParameters } from 'three' import SpeckleStandardMaterial from './SpeckleStandardMaterial' -import { Uniforms } from './SpeckleMaterial' +import { type Uniforms } from './SpeckleMaterial' class SpeckleStandardColoredMaterial extends SpeckleStandardMaterial { protected get vertexProgram(): string { @@ -17,7 +17,7 @@ class SpeckleStandardColoredMaterial extends SpeckleStandardMaterial { return { ...super.uniformsDef, gradientRamp: null } } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: MeshStandardMaterialParameters, defines = ['USE_RTE']) { super(parameters, defines) } diff --git a/packages/viewer/src/modules/materials/SpeckleStandardMaterial.ts b/packages/viewer/src/modules/materials/SpeckleStandardMaterial.ts index 96c8a1d8e7..49936ac208 100644 --- a/packages/viewer/src/modules/materials/SpeckleStandardMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleStandardMaterial.ts @@ -1,10 +1,19 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ import { speckleStandardVert } from './shaders/speckle-standard-vert' import { speckleStandardFrag } from './shaders/speckle-standard-frag' -import { ShaderLib, Vector3, Material, IUniform } from 'three' +import { + ShaderLib, + Vector3, + Material, + type IUniform, + type MeshStandardMaterialParameters, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' import { Matrix4 } from 'three' -import { ExtendedMeshStandardMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshStandardMaterial, type Uniforms } from './SpeckleMaterial' import { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleStandardMaterial extends ExtendedMeshStandardMaterial { @@ -36,7 +45,7 @@ class SpeckleStandardMaterial extends ExtendedMeshStandardMaterial { } } - constructor(parameters, defines = ['USE_RTE']) { + constructor(parameters: MeshStandardMaterialParameters, defines = ['USE_RTE']) { super(parameters) this.init(defines) } @@ -46,11 +55,13 @@ class SpeckleStandardMaterial extends ExtendedMeshStandardMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) - this.originalRoughness = source.originalRoughness - this.artificialRoughness = source.artificialRoughness + if (source instanceof SpeckleStandardMaterial) { + this.originalRoughness = source.originalRoughness + this.artificialRoughness = source.artificialRoughness + } return this } @@ -94,16 +105,26 @@ class SpeckleStandardMaterial extends ExtendedMeshStandardMaterial { if (this.originalRoughness === undefined) this.originalRoughness = this.roughness this.artificialRoughness = artificialRougness } + if (this.originalRoughness === undefined || this.artificialRoughness === undefined) + return + const applyRoughness = artificialRougness !== undefined ? Math.min(this.originalRoughness, this.artificialRoughness) : this.originalRoughness + this.roughness = applyRoughness this.needsCopy = true } /** Called by three.js render loop */ - public onBeforeRender(_this: SpeckleWebGLRenderer, scene, camera, geometry, object) { + public onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + _camera: Camera, + _geometry: BufferGeometry, + object: Object3D + ) { if (this.defines['USE_RTE']) { object.modelViewMatrix.copy(_this.RTEBuffers.rteViewModelMatrix) this.userData.uViewer_low.value.copy(_this.RTEBuffers.viewerLow) diff --git a/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts b/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts index 6dd11a5fa4..9eb3ac2038 100644 --- a/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleTextMaterial.ts @@ -2,17 +2,31 @@ /* eslint-disable camelcase */ import { speckleTextVert } from './shaders/speckle-text-vert' import { speckleTextFrag } from './shaders/speckle-text-frag' -import { ShaderLib, Vector3, IUniform, Vector2, Material } from 'three' +import { + ShaderLib, + Vector3, + type IUniform, + Vector2, + Material, + type MeshBasicMaterialParameters, + Scene, + Camera, + BufferGeometry, + Object3D +} from 'three' import { Matrix4 } from 'three' -import { ExtendedMeshBasicMaterial, Uniforms } from './SpeckleMaterial' +import { ExtendedMeshBasicMaterial, type Uniforms } from './SpeckleMaterial' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore import { createTextDerivedMaterial } from 'troika-three-text' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { protected static readonly matBuff: Matrix4 = new Matrix4() protected static readonly vecBuff: Vector2 = new Vector2() - private _billboardPixelHeight: number + private _billboardPixelHeight!: number protected get vertexProgram(): string { return speckleTextVert @@ -47,7 +61,7 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { return this._billboardPixelHeight } - constructor(parameters, defines = []) { + constructor(parameters: MeshBasicMaterialParameters, defines: Array = []) { super(parameters) this.init(defines) } @@ -57,7 +71,7 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { return this.constructor.name } - public copy(source) { + public copy(source: Material) { super.copy(source) this.copyFrom(source) return this @@ -83,8 +97,14 @@ class SpeckleTextMaterial extends ExtendedMeshBasicMaterial { } /** Called by three.js render loop */ - public onBeforeRender(_this, scene, camera, geometry, object, group) { - if (this.defines['BILLBOARD_FIXED']) { + public onBeforeRender( + _this: SpeckleWebGLRenderer, + _scene: Scene, + camera: Camera, + _geometry: BufferGeometry, + _object: Object3D + ) { + if (this.defines && this.defines['BILLBOARD_FIXED']) { const resolution = _this.getDrawingBufferSize(SpeckleTextMaterial.vecBuff) SpeckleTextMaterial.vecBuff.set( (this._billboardPixelHeight / resolution.x) * 2, diff --git a/packages/viewer/src/modules/objects/AccelerationStructure.ts b/packages/viewer/src/modules/objects/AccelerationStructure.ts index d34cb7f18f..a2a8aabac4 100644 --- a/packages/viewer/src/modules/objects/AccelerationStructure.ts +++ b/packages/viewer/src/modules/objects/AccelerationStructure.ts @@ -3,16 +3,13 @@ import { BufferGeometry, Float32BufferAttribute, FrontSide, - Intersection, Material, Matrix4, - Object3D, Ray, Side, Uint16BufferAttribute, Uint32BufferAttribute, - Vector3, - Event + Vector3 } from 'three' import { CENTER, @@ -21,6 +18,7 @@ import { ShapecastIntersection, SplitStrategy } from 'three-mesh-bvh' +import { MeshIntersection } from './SpeckleRaycaster' const SKIP_GENERATION = Symbol('skip tree generation') @@ -31,7 +29,7 @@ export interface BVHOptions { verbose: boolean useSharedArrayBuffer: boolean setBoundingBox: boolean - onProgress: () => void + onProgress?: () => void [SKIP_GENERATION]: boolean } @@ -42,7 +40,6 @@ export const DefaultBVHOptions = { verbose: true, useSharedArrayBuffer: false, setBoundingBox: false, - onProgress: null, [SKIP_GENERATION]: false } @@ -71,10 +68,10 @@ to get the correct values for Vectors, Rays, Boxes, etc export class AccelerationStructure { private static readonly MatBuff: Matrix4 = new Matrix4() private _bvh: MeshBVH - public inputTransform: Matrix4 - public outputTransform: Matrix4 - public inputOriginTransform: Matrix4 - public outputOriginTransfom: Matrix4 + public inputTransform!: Matrix4 + public outputTransform!: Matrix4 + public inputOriginTransform!: Matrix4 + public outputOriginTransfom!: Matrix4 public get geometry() { return this._bvh.geometry @@ -85,12 +82,17 @@ export class AccelerationStructure { } public static buildBVH( - indices: number[], - position: Float32Array, + indices: number[] | undefined, + position: number[] | undefined, options: BVHOptions = DefaultBVHOptions, transform?: Matrix4 ): MeshBVH { - let bvhPositions = position + /** There is no unniverse where you can build a BVH without proper indices or positions */ + if (!indices || !position) { + throw new Error('Cannot build BVH with undefined indices or position!') + } + + let bvhPositions = new Float32Array(position) if (transform) { bvhPositions = new Float32Array(position.length) const vecBuff = new Vector3() @@ -129,21 +131,23 @@ export class AccelerationStructure { public raycast( ray: Ray, materialOrSide: Side | Material | Material[] = FrontSide - ): Intersection>[] { + ): MeshIntersection[] { const res = this._bvh.raycast(this.transformInput(ray), materialOrSide) res.forEach((value) => { value.point = this.transformOutput(value.point) }) - return res + /** The intersection results from raycasting a bvh will always overlap with MeshIntersection because the bvh uses indexed geometry */ + return res as MeshIntersection[] } public raycastFirst( ray: Ray, materialOrSide: Side | Material | Material[] = FrontSide - ): Intersection> { + ): MeshIntersection { const res = this._bvh.raycastFirst(this.transformInput(ray), materialOrSide) res.point = this.transformOutput(res.point) - return res + /** The intersection results from raycasting a bvh will always overlap with MeshIntersection because the bvh uses indexed geometry */ + return res as MeshIntersection } public shapecast( @@ -274,9 +278,10 @@ export class AccelerationStructure { return output.applyMatrix4(AccelerationStructure.MatBuff) as T } - public getBoundingBox(target) { - this._bvh.getBoundingBox(target) - return this.transformOutput(target) + public getBoundingBox(target?: Box3): Box3 { + const _target: Box3 = target ? target : new Box3() + this._bvh.getBoundingBox(_target) + return this.transformOutput(_target) } public getVertexAtIndex(index: number): Vector3 { diff --git a/packages/viewer/src/modules/objects/RotatablePMREMGenerator.ts b/packages/viewer/src/modules/objects/RotatablePMREMGenerator.ts index 9a11f1204a..9d9638f0d1 100644 --- a/packages/viewer/src/modules/objects/RotatablePMREMGenerator.ts +++ b/packages/viewer/src/modules/objects/RotatablePMREMGenerator.ts @@ -1,7 +1,13 @@ -import { Matrix4, NoBlending, PMREMGenerator, ShaderMaterial } from 'three' +import { + Matrix4, + NoBlending, + PMREMGenerator, + ShaderMaterial, + WebGLRenderer +} from 'three' export class RotatablePMREMGenerator extends PMREMGenerator { - constructor(renderer) { + constructor(renderer: WebGLRenderer) { super(renderer) } diff --git a/packages/viewer/src/modules/objects/SpeckleCamera.ts b/packages/viewer/src/modules/objects/SpeckleCamera.ts index e199ceaf8d..39e00cda30 100644 --- a/packages/viewer/src/modules/objects/SpeckleCamera.ts +++ b/packages/viewer/src/modules/objects/SpeckleCamera.ts @@ -1,10 +1,17 @@ import { Box3, OrthographicCamera, PerspectiveCamera } from 'three' export enum CameraEvent { - Stationary, - Dynamic, - FrameUpdate, - ProjectionChanged + Stationary = 'stationary', + Dynamic = 'dynamic', + FrameUpdate = 'frame-update', + ProjectionChanged = 'projection-changed' +} + +export interface CameraEventPayload { + [CameraEvent.Stationary]: void + [CameraEvent.Dynamic]: void + [CameraEvent.FrameUpdate]: boolean + [CameraEvent.ProjectionChanged]: CameraProjection } export interface SpeckleCamera { @@ -12,8 +19,11 @@ export interface SpeckleCamera { get fieldOfView(): number set fieldOfView(value: number) get aspect(): number - on(type: CameraEvent, handler: (...args) => void) - setCameraPlanes(targetVolume: Box3, offsetScale?: number) + on( + eventType: T, + listener: (arg: CameraEventPayload[T]) => void + ): void + setCameraPlanes(targetVolume: Box3, offsetScale?: number): void } export enum CameraProjection { PERSPECTIVE, diff --git a/packages/viewer/src/modules/objects/SpeckleCameraControls.ts b/packages/viewer/src/modules/objects/SpeckleCameraControls.ts index df1e1990d6..1c95ceffb9 100644 --- a/packages/viewer/src/modules/objects/SpeckleCameraControls.ts +++ b/packages/viewer/src/modules/objects/SpeckleCameraControls.ts @@ -1,52 +1,55 @@ import CameraControls from 'camera-controls' -import { MathUtils, PerspectiveCamera, Vector3 } from 'three' +import { MathUtils, OrthographicCamera, PerspectiveCamera, Vector3 } from 'three' -let ACTION -;(function (ACTION) { - ACTION[(ACTION['NONE'] = 0)] = 'NONE' - ACTION[(ACTION['ROTATE'] = 1)] = 'ROTATE' - ACTION[(ACTION['TRUCK'] = 2)] = 'TRUCK' - ACTION[(ACTION['OFFSET'] = 3)] = 'OFFSET' - ACTION[(ACTION['DOLLY'] = 4)] = 'DOLLY' - ACTION[(ACTION['ZOOM'] = 5)] = 'ZOOM' - ACTION[(ACTION['TOUCH_ROTATE'] = 6)] = 'TOUCH_ROTATE' - ACTION[(ACTION['TOUCH_TRUCK'] = 7)] = 'TOUCH_TRUCK' - ACTION[(ACTION['TOUCH_OFFSET'] = 8)] = 'TOUCH_OFFSET' - ACTION[(ACTION['TOUCH_DOLLY'] = 9)] = 'TOUCH_DOLLY' - ACTION[(ACTION['TOUCH_ZOOM'] = 10)] = 'TOUCH_ZOOM' - ACTION[(ACTION['TOUCH_DOLLY_TRUCK'] = 11)] = 'TOUCH_DOLLY_TRUCK' - ACTION[(ACTION['TOUCH_DOLLY_OFFSET'] = 12)] = 'TOUCH_DOLLY_OFFSET' - ACTION[(ACTION['TOUCH_ZOOM_TRUCK'] = 13)] = 'TOUCH_ZOOM_TRUCK' - ACTION[(ACTION['TOUCH_ZOOM_OFFSET'] = 14)] = 'TOUCH_ZOOM_OFFSET' -})(ACTION || (ACTION = {})) -function isPerspectiveCamera(camera) { - return camera.isPerspectiveCamera +enum ACTION { + NONE = 0, + ROTATE = 1, + TRUCK = 2, + OFFSET = 3, + DOLLY = 4, + ZOOM = 5, + TOUCH_ROTATE = 6, + TOUCH_TRUCK = 7, + TOUCH_OFFSET = 8, + TOUCH_DOLLY = 9, + TOUCH_ZOOM = 10, + TOUCH_DOLLY_TRUCK = 11, + TOUCH_DOLLY_OFFSET = 12, + TOUCH_ZOOM_TRUCK = 13, + TOUCH_ZOOM_OFFSET = 14 } -function isOrthographicCamera(camera) { - return camera.isOrthographicCamera + +function isPerspectiveCamera(camera: PerspectiveCamera | OrthographicCamera) { + return (camera as PerspectiveCamera).isPerspectiveCamera +} +function isOrthographicCamera(camera: PerspectiveCamera | OrthographicCamera) { + return (camera as OrthographicCamera).isOrthographicCamera } const EPSILON = 1e-5 -function approxZero(number, error = EPSILON) { +function approxZero(number: number, error = EPSILON) { return Math.abs(number) < error } -function approxEquals(a, b, error = EPSILON) { +function approxEquals(a: number, b: number, error = EPSILON) { return approxZero(a - b, error) } -let _deltaTarget, _deltaOffset, _v3A, _v3B, _v3C -let _xColumn -let _yColumn -let _zColumn +let _deltaTarget: Vector3, + _deltaOffset: Vector3, + _v3A: Vector3, + _v3B: Vector3, + _v3C: Vector3 +let _xColumn: Vector3 +let _yColumn: Vector3 +let _zColumn: Vector3 export class SpeckleCameraControls extends CameraControls { private _didDolly = false private _didDollyLastFrame = false public _isTrucking = false - private _hasRestedLastFrame = false private _didZoom = false - private overrideDollyLerpRatio = 0 - private overrideZoomLerpRatio = 0 + private overrideDollyLerpRatio: number | undefined = 0 + private overrideZoomLerpRatio: number | undefined = 0 static install() { _v3A = new Vector3() @@ -127,11 +130,7 @@ export class SpeckleCameraControls extends CameraControls { * @param enableTransition * @category Methods */ - zoomTo( - zoom: number, - enableTransition = false, - lerpRatio: number = undefined - ): Promise { + zoomTo(zoom: number, enableTransition = false, lerpRatio?: number): Promise { this._zoomEnd = MathUtils.clamp(zoom, this.minZoom, this.maxZoom) this._needsUpdate = true this.overrideZoomLerpRatio = enableTransition ? 0.05 : lerpRatio @@ -153,7 +152,7 @@ export class SpeckleCameraControls extends CameraControls { dollyTo( distance: number, enableTransition = true, - lerpRatio = undefined + lerpRatio?: number ): Promise { const lastRadius = this._sphericalEnd.radius const newRadius = MathUtils.clamp(distance, this.minDistance, this.maxDistance) @@ -193,8 +192,7 @@ export class SpeckleCameraControls extends CameraControls { return this._createOnRestPromise(resolveImmediately) } - update(delta) { - this._hasRestedLastFrame = this._hasRested + update(delta: number) { const dampingFactor = this._state === ACTION.NONE ? this.dampingFactor : this.draggingDampingFactor const lerpRatio = Math.min(dampingFactor * delta * 60, 1) diff --git a/packages/viewer/src/modules/objects/SpeckleInstancedMesh.ts b/packages/viewer/src/modules/objects/SpeckleInstancedMesh.ts index 75418eeac5..8c66d95bb7 100644 --- a/packages/viewer/src/modules/objects/SpeckleInstancedMesh.ts +++ b/packages/viewer/src/modules/objects/SpeckleInstancedMesh.ts @@ -1,5 +1,6 @@ import { BackSide, + BufferAttribute, BufferGeometry, DoubleSide, Group, @@ -7,23 +8,26 @@ import { InstancedMesh, Material, Matrix4, + Object3D, Ray, Raycaster, + SkinnedMesh, Sphere, Triangle, Vector2, - Vector3 + Vector3, + type Intersection } from 'three' import { BatchObject } from '../batching/BatchObject' import Materials from '../materials/Materials' import { TopLevelAccelerationStructure } from './TopLevelAccelerationStructure' +import { ObjectLayers } from '../../IViewer' +import Logger from 'js-logger' import { - DrawGroup, + type DrawGroup, INSTANCE_GRADIENT_BUFFER_STRIDE, INSTANCE_TRANSFORM_BUFFER_STRIDE } from '../batching/Batch' -import { ObjectLayers } from '../../IViewer' -import Logger from 'js-logger' const _inverseMatrix = new Matrix4() const _ray = new Ray() @@ -54,20 +58,20 @@ const tmpInverseMatrix = /* @__PURE__ */ new Matrix4() export default class SpeckleInstancedMesh extends Group { public static MeshBatchNumber = 0 - private tas: TopLevelAccelerationStructure = null - private batchMaterial: Material = null + private tas: TopLevelAccelerationStructure + private batchMaterial: Material | null = null private materialCache: { [id: string]: Material } = {} - private materialStack: Array = [] + private materialStack: Array> = [] private materialCacheLUT: { [id: string]: number } = {} - private _batchObjects: BatchObject[] + private _batchObjects!: BatchObject[] public groups: Array = [] public materials: Material[] = [] - private instanceGeometry: BufferGeometry = null + private instanceGeometry: BufferGeometry | undefined = undefined private instances: InstancedMesh[] = [] - public get TAS() { + public get TAS(): TopLevelAccelerationStructure { return this.tas } @@ -92,8 +96,7 @@ export default class SpeckleInstancedMesh extends Group { } public setOverrideMaterial(material: Material) { - material - const saveMaterials = [] + const saveMaterials: Array = [] for (let k = 0; k < this.instances.length; k++) { saveMaterials.push(this.instances[k].material) } @@ -118,7 +121,11 @@ export default class SpeckleInstancedMesh extends Group { this.materialCacheLUT[clone.id] = material.id cachedMaterial = clone this.updateMaterialTransformsUniform(this.materialCache[material.id]) - } else if (copy || material['needsCopy'] || cachedMaterial['needsCopy']) { + } else if ( + copy || + (material as never)['needsCopy'] || + (cachedMaterial as never)['needsCopy'] + ) { Materials.fastCopy(material, cachedMaterial) } return cachedMaterial @@ -149,7 +156,8 @@ export default class SpeckleInstancedMesh extends Group { this.instances.length = 0 for (let k = 0; k < this.groups.length; k++) { - const material = this.materials[this.groups[k].materialIndex] + const materialIndex = this.groups[k].materialIndex + const material = this.materials[materialIndex] const group = new InstancedMesh(this.instanceGeometry, material, 0) group.instanceMatrix = new InstancedBufferAttribute( transformBuffer.subarray( @@ -231,7 +239,11 @@ export default class SpeckleInstancedMesh extends Group { // converts the given BVH raycast intersection to align with the three.js raycast // structure (include object, world space distance and point). - private convertRaycastIntersect(hit, object, raycaster) { + private convertRaycastIntersect( + hit: Intersection | null, + object: Object3D, + raycaster: Raycaster + ) { if (hit === null) { return null } @@ -247,9 +259,9 @@ export default class SpeckleInstancedMesh extends Group { } } - raycast(raycaster: Raycaster, intersects) { + raycast(raycaster: Raycaster, intersects: Array) { if (this.tas) { - if (this.batchMaterial === undefined) return + if (!this.batchMaterial) return tmpInverseMatrix.copy(this.matrixWorld).invert() ray.copy(raycaster.ray).applyMatrix4(tmpInverseMatrix) @@ -278,12 +290,13 @@ export default class SpeckleInstancedMesh extends Group { const matrixWorld = this.matrixWorld if (material === undefined) return + if (geometry === undefined) return // Checking boundingSphere distance to ray if (geometry.boundingSphere === null) geometry.computeBoundingSphere() - _sphere.copy(geometry.boundingSphere) + _sphere.copy(geometry.boundingSphere || new Sphere()) _sphere.applyMatrix4(matrixWorld) if (raycaster.ray.intersectsSphere(_sphere) === false) return @@ -303,13 +316,13 @@ export default class SpeckleInstancedMesh extends Group { const index = geometry.index /** Stored high component if RTE is being used. Regular positions otherwise */ - const position = geometry.attributes.position + const position = geometry.attributes.position as BufferAttribute /** Stored low component if RTE is being used. undefined otherwise */ - const positionLow = geometry.attributes['position_low'] - const morphPosition = geometry.morphAttributes.position + const positionLow = geometry.attributes['position_low'] as BufferAttribute + const morphPosition = geometry.morphAttributes.position as Array const morphTargetsRelative = geometry.morphTargetsRelative - const uv = geometry.attributes.uv - const uv2 = geometry.attributes.uv2 + const uv = geometry.attributes.uv as BufferAttribute + const uv2 = geometry.attributes.uv2 as BufferAttribute const groups = geometry.groups const drawRange = geometry.drawRange @@ -319,6 +332,10 @@ export default class SpeckleInstancedMesh extends Group { if (Array.isArray(material)) { for (let i = 0, il = groups.length; i < il; i++) { const group = groups[i] + if (!group.materialIndex) { + Logger.error(`Group with no material, skipping!`) + continue + } const groupMaterial = material[group.materialIndex] const start = Math.max(group.start, drawRange.start) @@ -350,7 +367,8 @@ export default class SpeckleInstancedMesh extends Group { if (intersection) { intersection.faceIndex = Math.floor(j / 3) // triangle number in indexed buffer semantics - intersection.face.materialIndex = group.materialIndex + if (intersection.face) + intersection.face.materialIndex = group.materialIndex as number intersects.push(intersection) } } @@ -392,6 +410,10 @@ export default class SpeckleInstancedMesh extends Group { if (Array.isArray(material)) { for (let i = 0, il = groups.length; i < il; i++) { const group = groups[i] + if (!group.materialIndex) { + Logger.error(`Group with no material, skipping!`) + continue + } const groupMaterial = material[group.materialIndex] const start = Math.max(group.start, drawRange.start) @@ -423,7 +445,8 @@ export default class SpeckleInstancedMesh extends Group { if (intersection) { intersection.faceIndex = Math.floor(j / 3) // triangle number in non-indexed buffer semantics - intersection.face.materialIndex = group.materialIndex + if (intersection.face) + intersection.face.materialIndex = group.materialIndex as number intersects.push(intersection) } } @@ -464,7 +487,16 @@ export default class SpeckleInstancedMesh extends Group { } } -function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) { +function checkIntersection( + object: Object3D, + material: Material, + raycaster: Raycaster, + ray: Ray, + pA: Vector3, + pB: Vector3, + pC: Vector3, + point: Vector3 +): (Intersection & { uv2: Vector2 | undefined }) | null { let intersect if (material.side === BackSide) { @@ -488,7 +520,8 @@ function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) object, uv: undefined, uv2: undefined, - face: undefined + face: undefined, + faceIndex: undefined } } @@ -496,19 +529,19 @@ function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) * hold the default `position` attribute values */ function checkBufferGeometryIntersection( - object, - material, - raycaster, - ray, - positionLow, - positionHigh, - morphPosition, - morphTargetsRelative, - uv, - uv2, - a, - b, - c + object: Object3D, + material: Material, + raycaster: Raycaster, + ray: Ray, + positionLow: BufferAttribute, + positionHigh: BufferAttribute, + morphPosition: Array, + morphTargetsRelative: boolean, + uv: BufferAttribute, + uv2: BufferAttribute, + a: number, + b: number, + c: number ) { _vA.fromBufferAttribute(positionHigh, a) _vB.fromBufferAttribute(positionHigh, b) @@ -519,7 +552,7 @@ function checkBufferGeometryIntersection( _vC.add(_vTemp.fromBufferAttribute(positionLow, c)) } - const morphInfluences = object.morphTargetInfluences + const morphInfluences = (object as SkinnedMesh).morphTargetInfluences if (morphPosition && morphInfluences) { _morphA.set(0, 0, 0) @@ -552,10 +585,10 @@ function checkBufferGeometryIntersection( _vC.add(_morphC) } - if (object.isSkinnedMesh) { - object.boneTransform(a, _vA) - object.boneTransform(b, _vB) - object.boneTransform(c, _vC) + if ((object as SkinnedMesh).isSkinnedMesh) { + ;(object as SkinnedMesh).boneTransform(a, _vA) + ;(object as SkinnedMesh).boneTransform(b, _vB) + ;(object as SkinnedMesh).boneTransform(c, _vC) } const intersection = checkIntersection( diff --git a/packages/viewer/src/modules/objects/SpeckleMesh.ts b/packages/viewer/src/modules/objects/SpeckleMesh.ts index d128cbcf17..7bf2565f76 100644 --- a/packages/viewer/src/modules/objects/SpeckleMesh.ts +++ b/packages/viewer/src/modules/objects/SpeckleMesh.ts @@ -1,7 +1,9 @@ import Logger from 'js-logger' import { BackSide, + Box3, Box3Helper, + BufferAttribute, BufferGeometry, Color, DataTexture, @@ -10,13 +12,16 @@ import { Material, Matrix4, Mesh, + Object3D, Ray, Raycaster, RGBAFormat, + SkinnedMesh, Sphere, Triangle, Vector2, - Vector3 + Vector3, + type Intersection } from 'three' import { BatchObject } from '../batching/BatchObject' import Materials from '../materials/Materials' @@ -57,23 +62,23 @@ export enum TransformStorage { export default class SpeckleMesh extends Mesh { public static MeshBatchNumber = 0 - private tas: TopLevelAccelerationStructure = null - private batchMaterial: Material = null + private tas: TopLevelAccelerationStructure + private batchMaterial: Material private materialCache: { [id: string]: Material } = {} private materialStack: Array = [] private materialCacheLUT: { [id: string]: number } = {} - private _batchObjects: BatchObject[] - private transformsBuffer: Float32Array = null - private transformStorage: TransformStorage + private _batchObjects!: BatchObject[] + private transformsBuffer: Float32Array | undefined = undefined + private transformStorage!: TransformStorage - public transformsTextureUniform: DataTexture = null - public transformsArrayUniforms: Matrix4[] = null + public transformsTextureUniform: DataTexture + public transformsArrayUniforms: Matrix4[] | null = null private debugBatchBox = false - private boxHelper: Box3Helper + private boxHelper!: Box3Helper - public get TAS() { + public get TAS(): TopLevelAccelerationStructure { return this.tas } @@ -134,17 +139,23 @@ export default class SpeckleMesh extends Mesh { this.materialCacheLUT[clone.id] = material.id cachedMaterial = clone this.updateMaterialTransformsUniform(this.materialCache[material.id]) - } else if (copy || material['needsCopy'] || cachedMaterial['needsCopy']) { + } else if ( + copy || + (material as never)['needsCopy'] || + (cachedMaterial as never)['needsCopy'] + ) { Materials.fastCopy(material, cachedMaterial) } return cachedMaterial } public restoreMaterial() { - if (this.materialStack.length > 0) this.material = this.materialStack.pop() + if (this.materialStack.length > 0) + this.material = this.materialStack.pop() as Material | Material[] } public updateMaterialTransformsUniform(material: Material) { + if (!material.defines) material.defines = {} material.defines['TRANSFORM_STORAGE'] = this.transformStorage if (this.transformStorage === TransformStorage.VERTEX_TEXTURE) { @@ -165,6 +176,7 @@ export default class SpeckleMesh extends Mesh { } public updateTransformsUniform() { + if (!this.transformsBuffer) return let needsUpdate = false if (this.transformStorage === TransformStorage.VERTEX_TEXTURE) { for (let k = 0; k < this._batchObjects.length; k++) { @@ -196,6 +208,7 @@ export default class SpeckleMesh extends Mesh { } this.transformsTextureUniform.needsUpdate = needsUpdate } else { + if (!this.transformsArrayUniforms) return for (let k = 0; k < this._batchObjects.length; k++) { const batchObject = this._batchObjects[k] if (!(needsUpdate ||= batchObject.transformDirty)) continue @@ -223,12 +236,17 @@ export default class SpeckleMesh extends Mesh { if (this.tas && needsUpdate) { this.tas.refit() this.tas.getBoundingBox(this.tas.bounds) + /** Caterint to typescript + * There is no unniverse where the geomery bounding box/sphere is null at this point + */ + if (!this.geometry.boundingBox) this.geometry.boundingBox = new Box3() this.geometry.boundingBox.copy(this.tas.bounds) + if (!this.geometry.boundingSphere) this.geometry.boundingSphere = new Sphere() this.geometry.boundingBox.getBoundingSphere(this.geometry.boundingSphere) if (!this.boxHelper && this.debugBatchBox) { this.boxHelper = new Box3Helper(this.tas.bounds, new Color(0xff0000)) this.boxHelper.layers.set(ObjectLayers.PROPS) - this.parent.add(this.boxHelper) + if (this.parent) this.parent.add(this.boxHelper) } } } @@ -258,13 +276,17 @@ export default class SpeckleMesh extends Mesh { ) return null } - return this.material[group.materialIndex] + return this.material[group.materialIndex as number] } } // converts the given BVH raycast intersection to align with the three.js raycast // structure (include object, world space distance and point). - private convertRaycastIntersect(hit, object, raycaster) { + private convertRaycastIntersect( + hit: Intersection | null, + object: Object3D, + raycaster: Raycaster + ) { if (hit === null) { return null } @@ -280,7 +302,7 @@ export default class SpeckleMesh extends Mesh { } } - raycast(raycaster: Raycaster, intersects) { + raycast(raycaster: Raycaster, intersects: Array) { if (this.tas) { if (this.batchMaterial === undefined) return @@ -316,7 +338,7 @@ export default class SpeckleMesh extends Mesh { if (geometry.boundingSphere === null) geometry.computeBoundingSphere() - _sphere.copy(geometry.boundingSphere) + _sphere.copy(geometry.boundingSphere || new Sphere()) _sphere.applyMatrix4(matrixWorld) if (raycaster.ray.intersectsSphere(_sphere) === false) return @@ -336,13 +358,13 @@ export default class SpeckleMesh extends Mesh { const index = geometry.index /** Stored high component if RTE is being used. Regular positions otherwise */ - const position = geometry.attributes.position + const position = geometry.attributes.position as BufferAttribute /** Stored low component if RTE is being used. undefined otherwise */ - const positionLow = geometry.attributes['position_low'] - const morphPosition = geometry.morphAttributes.position + const positionLow = geometry.attributes['position_low'] as BufferAttribute + const morphPosition = geometry.morphAttributes.position as Array const morphTargetsRelative = geometry.morphTargetsRelative - const uv = geometry.attributes.uv - const uv2 = geometry.attributes.uv2 + const uv = geometry.attributes.uv as BufferAttribute + const uv2 = geometry.attributes.uv2 as BufferAttribute const groups = geometry.groups const drawRange = geometry.drawRange @@ -352,6 +374,10 @@ export default class SpeckleMesh extends Mesh { if (Array.isArray(material)) { for (let i = 0, il = groups.length; i < il; i++) { const group = groups[i] + if (!group.materialIndex) { + Logger.error(`Group with no material, skipping!`) + continue + } const groupMaterial = material[group.materialIndex] const start = Math.max(group.start, drawRange.start) @@ -383,7 +409,8 @@ export default class SpeckleMesh extends Mesh { if (intersection) { intersection.faceIndex = Math.floor(j / 3) // triangle number in indexed buffer semantics - intersection.face.materialIndex = group.materialIndex + if (intersection.face) + intersection.face.materialIndex = group.materialIndex as number intersects.push(intersection) } } @@ -425,7 +452,7 @@ export default class SpeckleMesh extends Mesh { if (Array.isArray(material)) { for (let i = 0, il = groups.length; i < il; i++) { const group = groups[i] - const groupMaterial = material[group.materialIndex] + const groupMaterial = material[group.materialIndex as number] const start = Math.max(group.start, drawRange.start) const end = Math.min( @@ -456,7 +483,8 @@ export default class SpeckleMesh extends Mesh { if (intersection) { intersection.faceIndex = Math.floor(j / 3) // triangle number in non-indexed buffer semantics - intersection.face.materialIndex = group.materialIndex + if (intersection.face) + intersection.face.materialIndex = group.materialIndex as number intersects.push(intersection) } } @@ -497,7 +525,16 @@ export default class SpeckleMesh extends Mesh { } } -function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) { +function checkIntersection( + object: Object3D, + material: Material, + raycaster: Raycaster, + ray: Ray, + pA: Vector3, + pB: Vector3, + pC: Vector3, + point: Vector3 +): (Intersection & { uv2: Vector2 | undefined }) | null { let intersect if (material.side === BackSide) { @@ -529,19 +566,19 @@ function checkIntersection(object, material, raycaster, ray, pA, pB, pC, point) * hold the default `position` attribute values */ function checkBufferGeometryIntersection( - object, - material, - raycaster, - ray, - positionLow, - positionHigh, - morphPosition, - morphTargetsRelative, - uv, - uv2, - a, - b, - c + object: Object3D, + material: Material, + raycaster: Raycaster, + ray: Ray, + positionLow: BufferAttribute, + positionHigh: BufferAttribute, + morphPosition: Array, + morphTargetsRelative: boolean, + uv: BufferAttribute, + uv2: BufferAttribute, + a: number, + b: number, + c: number ) { _vA.fromBufferAttribute(positionHigh, a) _vB.fromBufferAttribute(positionHigh, b) @@ -552,7 +589,7 @@ function checkBufferGeometryIntersection( _vC.add(_vTemp.fromBufferAttribute(positionLow, c)) } - const morphInfluences = object.morphTargetInfluences + const morphInfluences = (object as SkinnedMesh).morphTargetInfluences if (morphPosition && morphInfluences) { _morphA.set(0, 0, 0) @@ -585,10 +622,10 @@ function checkBufferGeometryIntersection( _vC.add(_morphC) } - if (object.isSkinnedMesh) { - object.boneTransform(a, _vA) - object.boneTransform(b, _vB) - object.boneTransform(c, _vC) + if ((object as SkinnedMesh).isSkinnedMesh) { + ;(object as SkinnedMesh).boneTransform(a, _vA) + ;(object as SkinnedMesh).boneTransform(b, _vB) + ;(object as SkinnedMesh).boneTransform(c, _vC) } const intersection = checkIntersection( diff --git a/packages/viewer/src/modules/objects/SpeckleRaycaster.ts b/packages/viewer/src/modules/objects/SpeckleRaycaster.ts index c9a223a26a..f94912ab76 100644 --- a/packages/viewer/src/modules/objects/SpeckleRaycaster.ts +++ b/packages/viewer/src/modules/objects/SpeckleRaycaster.ts @@ -1,7 +1,17 @@ -import { Box3, Intersection, Material, Object3D, Raycaster } from 'three' +import { + Box3, + type Intersection, + Object3D, + Raycaster, + Vector3, + RaycasterParameters, + Face +} from 'three' import { ExtendedTriangle, ShapecastIntersection } from 'three-mesh-bvh' import { BatchObject } from '../batching/BatchObject' import { ObjectLayers } from '../../IViewer' +import SpeckleMesh from './SpeckleMesh' +import SpeckleInstancedMesh from './SpeckleInstancedMesh' export type ExtendedShapeCastCallbacks = { intersectsTAS?: ( @@ -45,13 +55,29 @@ export type ExtendedShapeCastCallbacks = { export interface ExtendedIntersection extends Intersection { batchObject?: BatchObject - material?: Material + pointOnLine?: Vector3 + // material?: Material +} + +export interface MeshIntersection extends Intersection { + face: Face + faceIndex: number +} + +export interface ExtendedMeshIntersection extends MeshIntersection { + batchObject: BatchObject + object: SpeckleMesh | SpeckleInstancedMesh +} + +export interface ExtendedRaycasterParameters extends RaycasterParameters { + Line2: { threshold: number } } export class SpeckleRaycaster extends Raycaster { - public onObjectIntersectionTest: (object: Object3D) => void = null + public onObjectIntersectionTest: ((object: Object3D) => void) | null = null + public params: ExtendedRaycasterParameters - constructor(origin?, direction?, near = 0, far = Infinity) { + constructor(origin?: Vector3, direction?: Vector3, near = 0, far = Infinity) { super(origin, direction, near, far) this.layers.disableAll() this.layers.enable(ObjectLayers.STREAM_CONTENT) @@ -61,9 +87,10 @@ export class SpeckleRaycaster extends Raycaster { this.layers.enable(ObjectLayers.STREAM_CONTENT_POINT_CLOUD) // OFF by default this.layers.enable(ObjectLayers.STREAM_CONTENT_POINT) + this.params = { Line2: { threshold: 0 } } } - public intersectObjects(objects, recursive = true, intersects = []) { + public intersectObjects(objects: Array, recursive = true, intersects = []) { for (let i = 0, l = objects.length; i < l; i++) { intersectObject(objects[i], this, intersects, recursive) } @@ -74,11 +101,16 @@ export class SpeckleRaycaster extends Raycaster { } } -function ascSort(a, b) { +function ascSort(a: Intersection, b: Intersection) { return a.distance - b.distance } -function intersectObject(object, raycaster, intersects, recursive) { +function intersectObject( + object: Object3D, + raycaster: SpeckleRaycaster, + intersects: Array, + recursive: boolean +) { if (object.layers.test(raycaster.layers)) { if (raycaster.onObjectIntersectionTest) { raycaster.onObjectIntersectionTest(object) @@ -87,7 +119,9 @@ function intersectObject(object, raycaster, intersects, recursive) { } recursive &&= // eslint-disable-next-line eqeqeq - object.userData.raycastChildren != undefined ? object.raycastChildren : true + object.userData.raycastChildren != undefined + ? object.userData.raycastChildren + : true if (recursive === true) { const children = object.children diff --git a/packages/viewer/src/modules/objects/SpeckleText.ts b/packages/viewer/src/modules/objects/SpeckleText.ts index 6610d60c03..589522f15a 100644 --- a/packages/viewer/src/modules/objects/SpeckleText.ts +++ b/packages/viewer/src/modules/objects/SpeckleText.ts @@ -8,14 +8,17 @@ import { MeshBasicMaterial, PlaneGeometry, Quaternion, + Raycaster, Vector2, Vector3, - Vector4 + Vector4, + type Intersection } from 'three' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore import { Text } from 'troika-three-text' -import { SpeckleObject } from '../tree/DataTree' import SpeckleBasicMaterial from '../materials/SpeckleBasicMaterial' -import { ObjectLayers } from '../../IViewer' +import { ObjectLayers, type SpeckleObject } from '../../IViewer' export interface SpeckleTextParams { textValue?: string @@ -26,7 +29,7 @@ export interface SpeckleTextParams { } export interface SpeckleTextStyle { - backgroundColor?: Color + backgroundColor?: Color | null backgroundCornerRadius?: number backgroundPixelHeight?: number textColor?: Color @@ -44,7 +47,7 @@ const DefaultSpeckleTextStyle: SpeckleTextStyle = { export class SpeckleText extends Mesh { private _layer: ObjectLayers = ObjectLayers.NONE private _text: Text = null - private _background: Mesh = null + private _background: Mesh | null = null private _backgroundSize: Vector3 = new Vector3() private _style: SpeckleTextStyle = Object.assign({}, DefaultSpeckleTextStyle) private _resolution: Vector2 = new Vector2() @@ -136,9 +139,11 @@ export class SpeckleText extends Mesh { if (position) { if (this._style.billboard) { this.textMesh.material.userData.billboardPos.value.copy(position) - ;( - this._background.material as SpeckleBasicMaterial - ).userData.billboardPos.value.copy(position) + if (this._background) { + ;( + this._background.material as SpeckleBasicMaterial + ).userData.billboardPos.value.copy(position) + } } this.position.copy(position) } @@ -146,7 +151,7 @@ export class SpeckleText extends Mesh { if (scale) this.scale.copy(scale) } - public raycast(raycaster, intersects) { + public raycast(raycaster: Raycaster, intersects: Array) { const { textRenderInfo, curveRadius } = this.textMesh if (textRenderInfo) { const bounds = textRenderInfo.blockBounds @@ -206,7 +211,7 @@ export class SpeckleText extends Mesh { raycastMesh.matrixWorld = this.textMesh.matrixWorld } raycastMesh.material.side = this.textMesh.material.side - const tempArray = [] + const tempArray: Array = [] raycastMesh.raycast(raycaster, tempArray) for (let i = 0; i < tempArray.length; i++) { tempArray[i].object = this @@ -221,7 +226,7 @@ export class SpeckleText extends Mesh { private updateBackground() { if (!this._style.backgroundColor) { - this.remove(this._background) + if (this._background) this.remove(this._background) this._background = null return } @@ -251,7 +256,9 @@ export class SpeckleText extends Mesh { const color = new Color(this._style.backgroundColor).convertSRGBToLinear() ;(this._background.material as SpeckleBasicMaterial).color = color ;(this._background.material as SpeckleBasicMaterial).billboardPixelHeight = - this._style.backgroundPixelHeight * window.devicePixelRatio + (this._style.backgroundPixelHeight !== undefined + ? this._style.backgroundPixelHeight + : DefaultSpeckleTextStyle.backgroundPixelHeight || 0) * window.devicePixelRatio } /** From https://discourse.threejs.org/t/roundedrectangle-squircle/28645 */ @@ -282,7 +289,7 @@ export class SpeckleText extends Mesh { return geometry - function contour(j) { + function contour(j: number) { qu = Math.trunc((4 * j) / n) + 1 // quadrant qu: 1..4 sgx = qu === 1 || qu === 4 ? 1 : -1 // signum left/right sgy = qu < 3 ? 1 : -1 // signum top / bottom diff --git a/packages/viewer/src/modules/objects/SpeckleWebGLRenderer.ts b/packages/viewer/src/modules/objects/SpeckleWebGLRenderer.ts index 2a08fe2b6d..c5af9c90a4 100644 --- a/packages/viewer/src/modules/objects/SpeckleWebGLRenderer.ts +++ b/packages/viewer/src/modules/objects/SpeckleWebGLRenderer.ts @@ -1,7 +1,7 @@ import { Camera, Matrix4, Vector3, WebGLRenderer } from 'three' import { Geometry } from '../converter/Geometry' export class RTEBuffers { - private _cache: RTEBuffers = null + private _cache: RTEBuffers | undefined viewer: Vector3 = new Vector3() viewerLow: Vector3 = new Vector3() diff --git a/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts b/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts index 6d2b2f08df..69cbb961f7 100644 --- a/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts +++ b/packages/viewer/src/modules/objects/TopLevelAccelerationStructure.ts @@ -1,19 +1,22 @@ import { Box3, Box3Helper, + BufferAttribute, Color, FrontSide, - Intersection, Material, Matrix4, - Object3D, Ray, Side, Vector3 } from 'three' import { ExtendedTriangle } from 'three-mesh-bvh' import { BatchObject } from '../batching/BatchObject' -import { ExtendedIntersection, ExtendedShapeCastCallbacks } from './SpeckleRaycaster' +import type { + ExtendedMeshIntersection, + ExtendedShapeCastCallbacks, + MeshIntersection +} from './SpeckleRaycaster' import { ObjectLayers } from '../../IViewer' import { AccelerationStructure } from './AccelerationStructure' @@ -55,8 +58,7 @@ export class TopLevelAccelerationStructure { public bounds: Box3 = new Box3(new Vector3(0, 0, 0), new Vector3(0, 0, 0)) public boxHelpers: Box3Helper[] = [] - public accelerationStructure: AccelerationStructure = null - public lastRefitTime = 0 + public accelerationStructure: AccelerationStructure public constructor(batchObjects: BatchObject[]) { this.batchObjects = batchObjects @@ -66,7 +68,7 @@ export class TopLevelAccelerationStructure { private buildBVH() { const indices = [] - const vertices = new Float32Array( + const vertices: number[] = new Array( TopLevelAccelerationStructure.CUBE_VERTS * 3 * this.batchObjects.length ) let vertOffset = 0 @@ -99,7 +101,7 @@ export class TopLevelAccelerationStructure { this.accelerationStructure.outputOriginTransfom = new Matrix4() } - private updateVertArray(box: Box3, offset: number, outPositions: Float32Array) { + private updateVertArray(box: Box3, offset: number, outPositions: number[]) { outPositions[offset] = box.min.x outPositions[offset + 1] = box.min.y outPositions[offset + 2] = box.max.z @@ -134,38 +136,39 @@ export class TopLevelAccelerationStructure { } public refit() { - const start = performance.now() - const positions = this.accelerationStructure.geometry.attributes.position.array + const positions = this.accelerationStructure.geometry.attributes.position + .array as number[] const boxBuffer: Box3 = new Box3() for (let k = 0; k < this.batchObjects.length; k++) { const start = this.batchObjects[k].tasVertIndexStart const basBox = this.batchObjects[k].accelerationStructure.getBoundingBox(boxBuffer) - this.updateVertArray(basBox, start * 3, positions as Float32Array) + this.updateVertArray(basBox, start * 3, positions) if (TopLevelAccelerationStructure.debugBoxes) this.boxHelpers[k].box.copy(basBox) } this.accelerationStructure.bvh.refit() - this.lastRefitTime = performance.now() - start } /* Core Cast Functions */ public raycast( ray: Ray, materialOrSide: Side | Material | Material[] = FrontSide - ): ExtendedIntersection[] { - const res = [] + ): ExtendedMeshIntersection[] { + const res: ExtendedMeshIntersection[] = [] const rayBuff = new Ray() rayBuff.copy(ray) - const tasResults: Intersection[] = this.accelerationStructure.raycast( + const tasResults: MeshIntersection[] = this.accelerationStructure.raycast( rayBuff, materialOrSide ) if (!tasResults.length) return res - tasResults.forEach((tasRes: Intersection) => { - const vertIndex = - this.accelerationStructure.geometry.index.array[tasRes.faceIndex * 3] + /** The index buffer for the bvh's geometry will *never* be undefined as it uses indexed geometry */ + const indexBufferAttribute: BufferAttribute = this.accelerationStructure.geometry + .index as BufferAttribute + tasResults.forEach((tasRes: MeshIntersection) => { + const vertIndex = indexBufferAttribute.array[tasRes.faceIndex * 3] const batchObjectIndex = Math.trunc( vertIndex / TopLevelAccelerationStructure.CUBE_VERTS ) @@ -175,9 +178,13 @@ export class TopLevelAccelerationStructure { materialOrSide ) hits.forEach((hit) => { - ;(hit as ExtendedIntersection).batchObject = this.batchObjects[batchObjectIndex] + /** We're promoting the MeshIntersection to ExtendedMeshIntersection because + * now we know it's corresponding batch object + */ + const extendedHit: ExtendedMeshIntersection = hit as ExtendedMeshIntersection + extendedHit.batchObject = this.batchObjects[batchObjectIndex] + res.push(extendedHit) }) - res.push(...hits) }) return res @@ -186,30 +193,33 @@ export class TopLevelAccelerationStructure { public raycastFirst( ray: Ray, materialOrSide: Side | Material | Material[] = FrontSide - ): ExtendedIntersection { - const res = null + ): ExtendedMeshIntersection | null { const rayBuff = new Ray() rayBuff.copy(ray) - const tasRes: Intersection = this.accelerationStructure.raycastFirst( + const tasRes: MeshIntersection = this.accelerationStructure.raycastFirst( rayBuff, materialOrSide ) - if (!tasRes) return res + if (!tasRes) return null - const vertIndex = - this.accelerationStructure.geometry.index.array[tasRes.faceIndex * 3] + /** The index buffer for the bvh's geometry will *never* be undefined as it uses indexed geometry */ + const indexBufferAttribute: BufferAttribute = this.accelerationStructure.geometry + .index as BufferAttribute + const vertIndex = indexBufferAttribute.array[tasRes.faceIndex * 3] const batchObjectIndex = Math.trunc( vertIndex / TopLevelAccelerationStructure.CUBE_VERTS ) rayBuff.copy(ray) - const hits = this.batchObjects[batchObjectIndex].accelerationStructure.raycast( - rayBuff, - materialOrSide - ) - hits.forEach((hit) => { - ;(hit as ExtendedIntersection).batchObject = this.batchObjects[batchObjectIndex] - }) - res.push(...hits) + const hit: MeshIntersection = this.batchObjects[ + batchObjectIndex + ].accelerationStructure.raycastFirst(rayBuff, materialOrSide) + /** We're promoting the MeshIntersection to ExtendedMeshIntersection because + * now we know it's corresponding batch object + */ + const extendedHit: ExtendedMeshIntersection = hit as ExtendedMeshIntersection + extendedHit.batchObject = this.batchObjects[batchObjectIndex] + + return extendedHit } public shapecast(callbacks: ExtendedShapeCastCallbacks): boolean { @@ -254,12 +264,15 @@ export class TopLevelAccelerationStructure { let ret = false this.accelerationStructure.shapecast({ intersectsBounds: (box, isLeaf, score, depth, nodeIndex) => { - const res = callbacks.intersectsTAS(box, isLeaf, score, depth, nodeIndex) - return res + if (callbacks.intersectsTAS) + return callbacks.intersectsTAS(box, isLeaf, score, depth, nodeIndex) + return false }, intersectsRange: (triangleOffset: number) => { - const vertIndex = - this.accelerationStructure.geometry.index.array[triangleOffset * 3] + /** The index buffer for the bvh's geometry will *never* be undefined as it uses indexed geometry */ + const indexBufferAttribute: BufferAttribute = this.accelerationStructure + .geometry.index as BufferAttribute + const vertIndex = indexBufferAttribute.array[triangleOffset * 3] const batchObjectIndex = Math.trunc( vertIndex / TopLevelAccelerationStructure.CUBE_VERTS ) diff --git a/packages/viewer/src/modules/pipeline/ApplyAOPass.ts b/packages/viewer/src/modules/pipeline/ApplyAOPass.ts index 66243f6ac9..6e2d526f9b 100644 --- a/packages/viewer/src/modules/pipeline/ApplyAOPass.ts +++ b/packages/viewer/src/modules/pipeline/ApplyAOPass.ts @@ -1,22 +1,24 @@ import { AddEquation, - Camera, CustomBlending, DstAlphaFactor, DstColorFactor, NoBlending, + OrthographicCamera, + PerspectiveCamera, Scene, ShaderMaterial, Texture, + WebGLRenderer, ZeroFactor } from 'three' import { FullScreenQuad, Pass } from 'three/examples/jsm/postprocessing/Pass.js' import { speckleApplyAoFrag } from '../materials/shaders/speckle-apply-ao-frag' import { speckleApplyAoVert } from '../materials/shaders/speckle-apply-ao-vert' import { - InputColorTextureUniform, - InputColorInterpolateTextureUniform, - SpeckleProgressivePass, + type InputColorTextureUniform, + type InputColorInterpolateTextureUniform, + type SpeckleProgressivePass, RenderType } from './SpecklePass' @@ -68,7 +70,7 @@ export class ApplySAOPass extends Pass implements SpeckleProgressivePass { return 'APPLYSAO' } - get outputTexture(): Texture { + get outputTexture(): Texture | null { return null } @@ -94,7 +96,7 @@ export class ApplySAOPass extends Pass implements SpeckleProgressivePass { this.materialCopy.needsUpdate = true } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { scene camera this.materialCopy.defines['NUM_FRAMES'] = this.accumulatioFrames @@ -102,9 +104,7 @@ export class ApplySAOPass extends Pass implements SpeckleProgressivePass { this.materialCopy.needsUpdate = true } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive*/) { - writeBuffer - readBuffer + render(renderer: WebGLRenderer) { renderer.setRenderTarget(null) const rendereAutoClear = renderer.autoClear renderer.autoClear = false diff --git a/packages/viewer/src/modules/pipeline/ColorPass.ts b/packages/viewer/src/modules/pipeline/ColorPass.ts index c43d06e08f..394fd16f0b 100644 --- a/packages/viewer/src/modules/pipeline/ColorPass.ts +++ b/packages/viewer/src/modules/pipeline/ColorPass.ts @@ -1,19 +1,29 @@ -import { Camera, Color, Material, Scene, Texture } from 'three' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { + Camera, + Color, + Material, + OrthographicCamera, + PerspectiveCamera, + Scene, + Texture, + WebGLRenderTarget, + WebGLRenderer +} from 'three' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class ColorPass extends BaseSpecklePass implements SpecklePass { - private camera: Camera - private scene: Scene - private overrideMaterial: Material = null + private camera: Camera | null = null + private scene: Scene | null = null + private overrideMaterial: Material private _oldClearColor: Color = new Color() - private clearColor: Color = null + private clearColor: Color private clearAlpha = 0 private clearDepth = true - public onBeforeRenderOpauqe: () => void = null - public onAfterRenderOpaque: () => void = null - public onBeforeRenderTransparent: () => void = null - public onAfterRenderTransparent: () => void = null + public onBeforeRenderOpauqe: (() => void) | undefined = undefined + public onAfterRenderOpaque: (() => void) | undefined = undefined + public onBeforeRenderTransparent: (() => void) | undefined = undefined + public onAfterRenderTransparent: (() => void) | undefined = undefined public constructor() { super() @@ -21,20 +31,26 @@ export class ColorPass extends BaseSpecklePass implements SpecklePass { public get displayName(): string { return 'COLOR' } - public get outputTexture(): Texture { + public get outputTexture(): Texture | null { return null } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { + render( + renderer: WebGLRenderer, + _writeBuffer: WebGLRenderTarget, + readBuffer: WebGLRenderTarget + ) { + if (!this.camera || !this.scene) return + const oldAutoClear = renderer.autoClear renderer.autoClear = false - let oldClearAlpha, oldOverrideMaterial + let oldClearAlpha, oldOverrideMaterial!: Material | null if (this.overrideMaterial !== undefined) { oldOverrideMaterial = this.scene.overrideMaterial diff --git a/packages/viewer/src/modules/pipeline/CopyOutputPass.ts b/packages/viewer/src/modules/pipeline/CopyOutputPass.ts index 1de9884acd..eea087451e 100644 --- a/packages/viewer/src/modules/pipeline/CopyOutputPass.ts +++ b/packages/viewer/src/modules/pipeline/CopyOutputPass.ts @@ -1,10 +1,16 @@ -import { NoBlending, ShaderMaterial, Texture, UniformsUtils } from 'three' +import { + NoBlending, + ShaderMaterial, + Texture, + UniformsUtils, + WebGLRenderer +} from 'three' import { FullScreenQuad, Pass } from 'three/examples/jsm/postprocessing/Pass.js' import { CopyShader } from 'three/examples/jsm/shaders/CopyShader.js' import { speckleCopyOutputFrag } from '../materials/shaders/speckle-copy-output-frag' import { speckleCopyOutputVert } from '../materials/shaders/speckle-copy-output-vert' import { PipelineOutputType } from './Pipeline' -import { InputColorTextureUniform, SpecklePass } from './SpecklePass' +import type { InputColorTextureUniform, SpecklePass } from './SpecklePass' export class CopyOutputPass extends Pass implements SpecklePass { private fsQuad: FullScreenQuad @@ -40,13 +46,11 @@ export class CopyOutputPass extends Pass implements SpecklePass { return 'COPY-OUTPUT' } - get outputTexture(): Texture { + get outputTexture(): Texture | null { return null } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive*/) { - writeBuffer - readBuffer + render(renderer: WebGLRenderer) { renderer.setRenderTarget(null) const rendereAutoClear = renderer.autoClear renderer.autoClear = false diff --git a/packages/viewer/src/modules/pipeline/DepthPass.ts b/packages/viewer/src/modules/pipeline/DepthPass.ts index 26d7bfb516..4f643df593 100644 --- a/packages/viewer/src/modules/pipeline/DepthPass.ts +++ b/packages/viewer/src/modules/pipeline/DepthPass.ts @@ -12,10 +12,11 @@ import { Scene, Side, Texture, - WebGLRenderTarget + WebGLRenderTarget, + WebGLRenderer } from 'three' import SpeckleDepthMaterial from '../materials/SpeckleDepthMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export enum DepthType { PERSPECTIVE_DEPTH, @@ -30,15 +31,15 @@ export enum DepthSize { export class DepthPass extends BaseSpecklePass implements SpecklePass { private renderTarget: WebGLRenderTarget private renderTargetHalf: WebGLRenderTarget - private depthMaterial: SpeckleDepthMaterial = null + private depthMaterial: SpeckleDepthMaterial private depthBufferSize: DepthSize = DepthSize.FULL - private scene: Scene - private camera: Camera + private scene: Scene | null + private camera: Camera | null private colorBuffer: Color = new Color() - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined get displayName(): string { return 'DEPTH' @@ -58,8 +59,16 @@ export class DepthPass extends BaseSpecklePass implements SpecklePass { public set depthType(value: DepthType) { if (value === DepthType.LINEAR_DEPTH) - this.depthMaterial.defines['LINEAR_DEPTH'] = ' ' - else delete this.depthMaterial.defines['LINEAR_DEPTH'] + if (this.depthMaterial.defines) { + /** Catering to typescript + * SpeckleDepthMaterial always has it's 'defines' defined + */ + this.depthMaterial.defines['LINEAR_DEPTH'] = ' ' + } else { + if (this.depthMaterial.defines) { + delete this.depthMaterial.defines['LINEAR_DEPTH'] + } + } this.depthMaterial.needsUpdate = true } @@ -107,7 +116,7 @@ export class DepthPass extends BaseSpecklePass implements SpecklePass { this.depthMaterial.clippingPlanes = planes } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene this.depthMaterial.userData.near.value = ( @@ -119,11 +128,10 @@ export class DepthPass extends BaseSpecklePass implements SpecklePass { this.depthMaterial.needsUpdate = true } - public render(renderer, writeBuffer, readBuffer) { - writeBuffer - readBuffer + public render(renderer: WebGLRenderer) { + if (!this.camera || !this.scene) return - this.onBeforeRender() + if (this.onBeforeRender) this.onBeforeRender() renderer.getClearColor(this.colorBuffer) const originalClearAlpha = renderer.getClearAlpha() const originalAutoClear = renderer.autoClear @@ -154,7 +162,7 @@ export class DepthPass extends BaseSpecklePass implements SpecklePass { renderer.autoClear = originalAutoClear renderer.setClearColor(this.colorBuffer) renderer.setClearAlpha(originalClearAlpha) - this.onAfterRender() + if (this.onAfterRender) this.onAfterRender() } public setSize(width: number, height: number) { diff --git a/packages/viewer/src/modules/pipeline/DynamicAOPass.ts b/packages/viewer/src/modules/pipeline/DynamicAOPass.ts index ba96eff665..ebf9cabdd5 100644 --- a/packages/viewer/src/modules/pipeline/DynamicAOPass.ts +++ b/packages/viewer/src/modules/pipeline/DynamicAOPass.ts @@ -1,5 +1,4 @@ import { - Camera, Color, NoBlending, OrthographicCamera, @@ -9,7 +8,8 @@ import { Texture, UniformsUtils, Vector2, - WebGLRenderTarget + WebGLRenderTarget, + WebGLRenderer } from 'three' import { FullScreenQuad, Pass } from 'three/examples/jsm/postprocessing/Pass.js' import { speckleSaoFrag } from '../materials/shaders/speckle-sao-frag' @@ -17,7 +17,7 @@ import { speckleSaoVert } from '../materials/shaders/speckle-sao-vert' import { SAOShader } from 'three/examples/jsm/shaders/SAOShader.js' import { DepthLimitedBlurShader } from 'three/examples/jsm/shaders/DepthLimitedBlurShader.js' import { BlurShaderUtils } from 'three/examples/jsm/shaders/DepthLimitedBlurShader.js' -import { +import type { InputDepthTextureUniform, InputNormalsTextureUniform, SpecklePass @@ -62,17 +62,17 @@ export const DefaultDynamicAOPassParams = { export class DynamicSAOPass extends Pass implements SpecklePass { private params: DynamicAOPassParams = DefaultDynamicAOPassParams private colorBuffer: Color = new Color() - private saoMaterial: ShaderMaterial = null - private vBlurMaterial: ShaderMaterial = null - private hBlurMaterial: ShaderMaterial = null - private saoRenderTarget: WebGLRenderTarget = null - private blurIntermediateRenderTarget: WebGLRenderTarget = null - private fsQuad: FullScreenQuad = null + private saoMaterial: ShaderMaterial + private vBlurMaterial: ShaderMaterial + private hBlurMaterial: ShaderMaterial + private saoRenderTarget: WebGLRenderTarget + private blurIntermediateRenderTarget: WebGLRenderTarget + private fsQuad: FullScreenQuad private _outputType: DynamicAOOutputType = DynamicAOOutputType.AO_BLURRED private outputScale = 0.5 - private prevStdDev: number - private prevNumSamples: number + private prevStdDev: number = 0 + private prevNumSamples: number = 0 public get displayName(): string { return 'SAO' @@ -163,7 +163,7 @@ export class DynamicSAOPass extends Pass implements SpecklePass { this.hBlurMaterial.needsUpdate = true } - public update(scene: Scene, camera: Camera) { + public update(_scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { if (this._outputType === DynamicAOOutputType.RECONSTRUCTED_NORMALS) { this.saoMaterial.defines['OUTPUT_RECONSTRUCTED_NORMALS'] = '' } else { @@ -260,7 +260,7 @@ export class DynamicSAOPass extends Pass implements SpecklePass { this.hBlurMaterial.needsUpdate = true } - public render(renderer) { + public render(renderer: WebGLRenderer) { // Rendering SAO texture renderer.getClearColor(this.colorBuffer) const originalClearAlpha = renderer.getClearAlpha() diff --git a/packages/viewer/src/modules/pipeline/NormalsPass.ts b/packages/viewer/src/modules/pipeline/NormalsPass.ts index a8dd4ddd53..180bb31cd2 100644 --- a/packages/viewer/src/modules/pipeline/NormalsPass.ts +++ b/packages/viewer/src/modules/pipeline/NormalsPass.ts @@ -4,24 +4,27 @@ import { DoubleSide, Material, NoBlending, + OrthographicCamera, + PerspectiveCamera, Plane, Scene, Texture, - WebGLRenderTarget + WebGLRenderTarget, + WebGLRenderer } from 'three' import SpeckleNormalMaterial from '../materials/SpeckleNormalMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class NormalsPass extends BaseSpecklePass implements SpecklePass { private renderTarget: WebGLRenderTarget - private normalsMaterial: SpeckleNormalMaterial = null + private normalsMaterial: SpeckleNormalMaterial private scene: Scene private camera: Camera private colorBuffer: Color = new Color() - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined get displayName(): string { return 'GEOMETRY-NORMALS' @@ -55,16 +58,13 @@ export class NormalsPass extends BaseSpecklePass implements SpecklePass { this.normalsMaterial.clippingPlanes = planes } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } - public render(renderer, writeBuffer, readBuffer) { - writeBuffer - readBuffer - - this.onBeforeRender() + public render(renderer: WebGLRenderer) { + if (this.onBeforeRender) this.onBeforeRender() renderer.getClearColor(this.colorBuffer) const originalClearAlpha = renderer.getClearAlpha() const originalAutoClear = renderer.autoClear @@ -91,7 +91,7 @@ export class NormalsPass extends BaseSpecklePass implements SpecklePass { renderer.autoClear = originalAutoClear renderer.setClearColor(this.colorBuffer) renderer.setClearAlpha(originalClearAlpha) - this.onAfterRender() + if (this.onAfterRender) this.onAfterRender() } public setSize(width: number, height: number) { diff --git a/packages/viewer/src/modules/pipeline/OverlayPass.ts b/packages/viewer/src/modules/pipeline/OverlayPass.ts index b779942884..825df75c2c 100644 --- a/packages/viewer/src/modules/pipeline/OverlayPass.ts +++ b/packages/viewer/src/modules/pipeline/OverlayPass.ts @@ -1,12 +1,20 @@ -import { Camera, Scene, Texture } from 'three' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { + Camera, + OrthographicCamera, + PerspectiveCamera, + Scene, + Texture, + WebGLRenderTarget, + WebGLRenderer +} from 'three' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class OverlayPass extends BaseSpecklePass implements SpecklePass { - private camera: Camera - private scene: Scene + private camera!: Camera + private scene!: Scene - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined public constructor() { super() @@ -14,16 +22,20 @@ export class OverlayPass extends BaseSpecklePass implements SpecklePass { public get displayName(): string { return 'OVERLAY' } - public get outputTexture(): Texture { + public get outputTexture(): Texture | null { return null } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { + render( + renderer: WebGLRenderer, + _writeBuffer: WebGLRenderTarget, + readBuffer: WebGLRenderTarget + ) { const oldAutoClear = renderer.autoClear renderer.autoClear = false this.applyLayers(this.camera) diff --git a/packages/viewer/src/modules/pipeline/Pipeline.ts b/packages/viewer/src/modules/pipeline/Pipeline.ts index 2783f13e51..8d48202647 100644 --- a/packages/viewer/src/modules/pipeline/Pipeline.ts +++ b/packages/viewer/src/modules/pipeline/Pipeline.ts @@ -1,8 +1,5 @@ import { DoubleSide, Plane, Side, Vector2, WebGLRenderer } from 'three' -import { - EffectComposer, - Pass -} from 'three/examples/jsm/postprocessing/EffectComposer.js' +import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' import Batcher from '../batching/Batcher' import SpeckleRenderer from '../SpeckleRenderer' import { ApplySAOPass } from './ApplyAOPass' @@ -13,20 +10,21 @@ import { DefaultDynamicAOPassParams, DynamicSAOPass, DynamicAOOutputType, - DynamicAOPassParams, + type DynamicAOPassParams, NormalsType } from './DynamicAOPass' import { DefaultStaticAoPassParams, StaticAOPass, - StaticAoPassParams + type StaticAoPassParams } from './StaticAOPass' -import { RenderType, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, RenderType, type SpecklePass } from './SpecklePass' import { ColorPass } from './ColorPass' import { StencilPass } from './StencilPass' import { StencilMaskPass } from './StencilMaskPass' import { OverlayPass } from './OverlayPass' import { ObjectLayers } from '../../IViewer' +import type { BatchUpdateRange } from '../batching/Batch' export enum PipelineOutputType { DEPTH_RGBA = 0, @@ -61,40 +59,42 @@ export const DefaultPipelineOptions: PipelineOptions = { } export class Pipeline { - private _renderer: WebGLRenderer = null - private _batcher: Batcher = null + private _renderer: WebGLRenderer + private _batcher: Batcher private _pipelineOptions: PipelineOptions = Object.assign({}, DefaultPipelineOptions) private _needsProgressive = false private _resetFrame = false - private _composer: EffectComposer = null - - private depthPass: DepthPass = null - private normalsPass: NormalsPass = null - private stencilPass: StencilPass = null - private renderPass: ColorPass = null - private stencilMaskPass: StencilMaskPass = null - private dynamicAoPass: DynamicSAOPass = null - private applySaoPass: ApplySAOPass = null - private copyOutputPass: CopyOutputPass = null - private staticAoPass: StaticAOPass = null - private overlayPass: OverlayPass = null + private _composer: EffectComposer + + private depthPass: DepthPass + private normalsPass: NormalsPass + private stencilPass: StencilPass + private renderPass: ColorPass + private stencilMaskPass: StencilMaskPass + private dynamicAoPass: DynamicSAOPass + private applySaoPass: ApplySAOPass + private copyOutputPass: CopyOutputPass + private staticAoPass: StaticAOPass + private overlayPass: OverlayPass private drawingSize: Vector2 = new Vector2() private _renderType: RenderType = RenderType.NORMAL private accumulationFrame = 0 - private onBeforePipelineRender = null - private onAfterPipelineRender = null + private onBeforePipelineRender: (() => void) | null = null + private onAfterPipelineRender: (() => void) | null = null public set pipelineOptions(options: Partial) { Object.assign(this._pipelineOptions, options) this.dynamicAoPass.setParams(options.dynamicAoParams) this.staticAoPass.setParams(options.staticAoParams) this.accumulationFrame = 0 - this.depthPass.depthSide = options.depthSide - this.applySaoPass.setAccumulationFrames(options.accumulationFrames) - this.staticAoPass.setAccumulationFrames(options.accumulationFrames) - this.pipelineOutput = options.pipelineOutput + if (options.depthSide) this.depthPass.depthSide = options.depthSide + if (options.accumulationFrames) { + this.applySaoPass.setAccumulationFrames(options.accumulationFrames) + this.staticAoPass.setAccumulationFrames(options.accumulationFrames) + } + if (options.pipelineOutput) this.pipelineOutput = options.pipelineOutput } public get pipelineOptions(): PipelineOptions { @@ -102,7 +102,7 @@ export class Pipeline { } public set pipelineOutput(outputType: PipelineOutputType) { - let pipeline = [] + let pipeline: Array = [] this.clearPipeline() switch (outputType) { case PipelineOutputType.FINAL: @@ -237,8 +237,11 @@ export class Pipeline { this._renderer = renderer this._batcher = batcher this._composer = new EffectComposer(renderer) - this._composer.readBuffer = null - this._composer.writeBuffer = null + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(this._composer as any).readBuffer = null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(this._composer as any).writeBuffer = null } public configure() { @@ -270,7 +273,10 @@ export class Pipeline { ]) this.stencilMaskPass.setLayers([ObjectLayers.STREAM_CONTENT_MESH]) this.overlayPass.setLayers([ObjectLayers.OVERLAY, ObjectLayers.MEASUREMENTS]) - let restoreVisibility, opaque, stencil, depth + let restoreVisibility: Record, + opaque: Record, + stencil: Record, + depth: Record this.onBeforePipelineRender = () => { restoreVisibility = this._batcher.saveVisiblity() @@ -381,7 +387,7 @@ export class Pipeline { private setPipeline(pipeline: Array) { for (let k = 0; k < pipeline.length; k++) { - this._composer.addPass(pipeline[k] as unknown as Pass) + this._composer.addPass(pipeline[k] as BaseSpecklePass) } } @@ -397,6 +403,8 @@ export class Pipeline { } public update(renderer: SpeckleRenderer) { + if (!renderer.scene || !renderer.renderingCamera) return + this.stencilPass.update(renderer.scene, renderer.renderingCamera) this.renderPass.update(renderer.scene, renderer.renderingCamera) this.stencilMaskPass.update(renderer.scene, renderer.renderingCamera) @@ -413,7 +421,7 @@ export class Pipeline { public render(): boolean { this._renderer.getDrawingBufferSize(this.drawingSize) - if (this.drawingSize.length() === 0) return + if (this.drawingSize.length() === 0) return false if (this.onBeforePipelineRender) this.onBeforePipelineRender() diff --git a/packages/viewer/src/modules/pipeline/ShadowcatcherPass.ts b/packages/viewer/src/modules/pipeline/ShadowcatcherPass.ts index 5e9738b065..be1fd89ebb 100644 --- a/packages/viewer/src/modules/pipeline/ShadowcatcherPass.ts +++ b/packages/viewer/src/modules/pipeline/ShadowcatcherPass.ts @@ -26,9 +26,13 @@ import { } from 'three/examples/jsm/shaders/DepthLimitedBlurShader.js' import SpeckleDepthMaterial from '../materials/SpeckleDepthMaterial' import SpeckleShadowcatcherMaterial from '../materials/SpeckleShadowcatcherMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' import { ObjectLayers } from '../../IViewer' -import { DefaultShadowcatcherConfig, ShadowcatcherConfig } from '../ShadowcatcherConfig' +import { + DefaultShadowcatcherConfig, + type ShadowcatcherConfig +} from '../ShadowcatcherConfig' +import type { SpeckleWebGLRenderer } from '../objects/SpeckleWebGLRenderer' export class ShadowcatcherPass extends BaseSpecklePass implements SpecklePass { private readonly levels: number = 4 @@ -36,23 +40,23 @@ export class ShadowcatcherPass extends BaseSpecklePass implements SpecklePass { private renderTargets: WebGLRenderTarget[] = [] private tempTargets: WebGLRenderTarget[] = [] private outputTarget: WebGLRenderTarget - private camera: OrthographicCamera = null - private scene: Scene = null + private camera: OrthographicCamera + private scene!: Scene private _needsUpdate = false - private fsQuad: FullScreenQuad = null - private blendMaterial: SpeckleShadowcatcherMaterial = null - private depthMaterial: SpeckleDepthMaterial = null - private vBlurMaterial: ShaderMaterial = null - private hBlurMaterial: ShaderMaterial = null + private fsQuad: FullScreenQuad + private blendMaterial: SpeckleShadowcatcherMaterial + private depthMaterial: SpeckleDepthMaterial + private vBlurMaterial: ShaderMaterial + private hBlurMaterial: ShaderMaterial private blurStdDev = DefaultShadowcatcherConfig.stdDeviation private blurRadius = DefaultShadowcatcherConfig.blurRadius private prevBlurStdDev = 0 private prevBlurRadius = 0 - private cameraHelper = null + private cameraHelper!: CameraHelper - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined get displayName(): string { return 'Shadowcatcher' @@ -124,7 +128,7 @@ export class ShadowcatcherPass extends BaseSpecklePass implements SpecklePass { public update(scene: Scene) { this.scene = scene if (this._needsUpdate) { - if (this.cameraHelper === null && this.debugCamera) { + if (!this.cameraHelper && this.debugCamera) { this.cameraHelper = new CameraHelper(this.camera) this.cameraHelper.layers.set(ObjectLayers.PROPS) this.scene.add(this.cameraHelper) @@ -180,9 +184,7 @@ export class ShadowcatcherPass extends BaseSpecklePass implements SpecklePass { } } - public render(renderer, writeBuffer, readBuffer) { - writeBuffer - readBuffer + public render(renderer: SpeckleWebGLRenderer) { if (this._needsUpdate) { renderer.RTEBuffers.push() renderer.updateRTEViewModel(this.camera) diff --git a/packages/viewer/src/modules/pipeline/SpecklePass.ts b/packages/viewer/src/modules/pipeline/SpecklePass.ts index feb2ce99ec..cc1430eca7 100644 --- a/packages/viewer/src/modules/pipeline/SpecklePass.ts +++ b/packages/viewer/src/modules/pipeline/SpecklePass.ts @@ -1,4 +1,11 @@ -import { Camera, Plane, Scene, Texture } from 'three' +import { + Camera, + OrthographicCamera, + PerspectiveCamera, + Plane, + Scene, + Texture +} from 'three' import { Pass } from 'three/examples/jsm/postprocessing/Pass.js' import { ObjectLayers } from '../../IViewer' @@ -17,23 +24,26 @@ export interface SpecklePass { onAferRender?: () => void get displayName(): string - get outputTexture(): Texture + get outputTexture(): Texture | null - update?(scene: Scene, camera: Camera) - setTexture?(uName: string, texture: Texture) - setParams?(params: unknown) - setClippingPlanes?(planes: Plane[]) - setLayers?(layers: ObjectLayers[]) + update?( + scene: Scene | null, + camera: PerspectiveCamera | OrthographicCamera | null + ): void + setTexture?(uName: string, texture: Texture): void + setParams?(params: unknown): void + setClippingPlanes?(planes: Plane[]): void + setLayers?(layers: ObjectLayers[]): void } export interface SpeckleProgressivePass extends SpecklePass { - setFrameIndex(index: number) - setAccumulationFrames(frames: number) - setRenderType?(type: RenderType) + setFrameIndex(index: number): void + setAccumulationFrames(frames: number): void + setRenderType?(type: RenderType): void } export abstract class BaseSpecklePass extends Pass implements SpecklePass { - protected layers: ObjectLayers[] = null + protected layers: ObjectLayers[] | null = null protected _enabledLayers: ObjectLayers[] = [] public get enabledLayers(): ObjectLayers[] { @@ -47,7 +57,7 @@ export abstract class BaseSpecklePass extends Pass implements SpecklePass { get displayName(): string { return 'BASE' } - get outputTexture(): Texture { + get outputTexture(): Texture | null { return null } diff --git a/packages/viewer/src/modules/pipeline/StaticAOPass.ts b/packages/viewer/src/modules/pipeline/StaticAOPass.ts index d3471b729d..68ac1a87e2 100644 --- a/packages/viewer/src/modules/pipeline/StaticAOPass.ts +++ b/packages/viewer/src/modules/pipeline/StaticAOPass.ts @@ -1,6 +1,5 @@ import { AddEquation, - Camera, Color, CustomBlending, DataTexture, @@ -29,7 +28,7 @@ import { speckleStaticAoGenerateFrag } from '../materials/shaders/speckle-static import { speckleStaticAoAccumulateVert } from '../materials/shaders/speckle-static-ao-accumulate-vert' import { speckleStaticAoAccumulateFrag } from '../materials/shaders/speckle-static-ao-accumulate-frag' import { SimplexNoise } from 'three/examples/jsm//math/SimplexNoise.js' -import { +import type { InputDepthTextureUniform, InputNormalsTextureUniform, SpeckleProgressivePass @@ -57,8 +56,8 @@ export const DefaultStaticAoPassParams = { } export class StaticAOPass extends Pass implements SpeckleProgressivePass { - public aoMaterial: ShaderMaterial = null - private accumulateMaterial: ShaderMaterial = null + public aoMaterial: ShaderMaterial + private accumulateMaterial: ShaderMaterial private _generationBuffer: WebGLRenderTarget private _accumulationBuffer: WebGLRenderTarget private params: StaticAoPassParams = DefaultStaticAoPassParams @@ -81,7 +80,7 @@ export class StaticAOPass extends Pass implements SpeckleProgressivePass { this.aoMaterial.needsUpdate = true } - public get outputTexture() { + public get outputTexture(): Texture { return this._accumulationBuffer.texture } @@ -164,7 +163,7 @@ export class StaticAOPass extends Pass implements SpeckleProgressivePass { this.accumulationFrames = frames } - public update(scene: Scene, camera: Camera) { + public update(_scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { /** DEFINES */ this.aoMaterial.defines['PERSPECTIVE_CAMERA'] = (camera as PerspectiveCamera) .isPerspectiveCamera @@ -209,9 +208,7 @@ export class StaticAOPass extends Pass implements SpeckleProgressivePass { this.accumulateMaterial.needsUpdate = true } - public render(renderer, writeBuffer, readBuffer) { - writeBuffer - readBuffer + public render(renderer: WebGLRenderer) { // save original state const originalClearColor = new Color() renderer.getClearColor(originalClearColor) @@ -254,7 +251,7 @@ export class StaticAOPass extends Pass implements SpeckleProgressivePass { } private generateSampleKernel(frameIndex: number) { - const kernelSize = this.params.kernelSize + const kernelSize = this.params.kernelSize || 0 this.kernels[frameIndex] = [] for (let i = 0; i < kernelSize; i++) { diff --git a/packages/viewer/src/modules/pipeline/StencilMaskPass.ts b/packages/viewer/src/modules/pipeline/StencilMaskPass.ts index 1d13642e0e..3195395717 100644 --- a/packages/viewer/src/modules/pipeline/StencilMaskPass.ts +++ b/packages/viewer/src/modules/pipeline/StencilMaskPass.ts @@ -4,27 +4,30 @@ import { DoubleSide, EqualStencilFunc, Material, + OrthographicCamera, + PerspectiveCamera, Plane, Scene, Texture, Vector2, + WebGLRenderTarget, WebGLRenderer } from 'three' import SpeckleDisplaceMaterial from '../materials/SpeckleDisplaceMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class StencilMaskPass extends BaseSpecklePass implements SpecklePass { - private camera: Camera - private scene: Scene - private overrideMaterial: Material = null + private camera!: Camera + private scene!: Scene + private overrideMaterial: Material private _oldClearColor: Color = new Color() - private clearColor: Color = null + private clearColor!: Color private clearAlpha = 0 private clearDepth = true private drawBufferSize: Vector2 = new Vector2() - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined public constructor() { super() @@ -42,7 +45,7 @@ export class StencilMaskPass extends BaseSpecklePass implements SpecklePass { public get displayName(): string { return 'STENCIL' } - public get outputTexture(): Texture { + public get outputTexture(): Texture | null { return null } @@ -50,7 +53,7 @@ export class StencilMaskPass extends BaseSpecklePass implements SpecklePass { return this.overrideMaterial } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } @@ -61,14 +64,15 @@ export class StencilMaskPass extends BaseSpecklePass implements SpecklePass { render( renderer: WebGLRenderer, - writeBuffer, - readBuffer /*, deltaTime, maskActive */ + _writeBuffer: WebGLRenderTarget, + readBuffer: WebGLRenderTarget ) { if (this.onBeforeRender) this.onBeforeRender() const oldAutoClear = renderer.autoClear renderer.autoClear = false - let oldClearAlpha, oldOverrideMaterial + let oldClearAlpha, + oldOverrideMaterial = null if (this.overrideMaterial !== undefined) { oldOverrideMaterial = this.scene.overrideMaterial diff --git a/packages/viewer/src/modules/pipeline/StencilPass.ts b/packages/viewer/src/modules/pipeline/StencilPass.ts index 4232b5c73f..7bb9bb1c7f 100644 --- a/packages/viewer/src/modules/pipeline/StencilPass.ts +++ b/packages/viewer/src/modules/pipeline/StencilPass.ts @@ -4,27 +4,31 @@ import { Color, DoubleSide, Material, + OrthographicCamera, + PerspectiveCamera, Plane, ReplaceStencilOp, Scene, Texture, - Vector2 + Vector2, + WebGLRenderTarget, + WebGLRenderer } from 'three' import SpeckleDisplaceMaterial from '../materials/SpeckleDisplaceMaterial' -import { BaseSpecklePass, SpecklePass } from './SpecklePass' +import { BaseSpecklePass, type SpecklePass } from './SpecklePass' export class StencilPass extends BaseSpecklePass implements SpecklePass { private camera: Camera private scene: Scene - private overrideMaterial: Material = null + private overrideMaterial: Material private _oldClearColor: Color = new Color() - private clearColor: Color = null + private clearColor: Color private clearAlpha = 0 private clearDepth = true private drawBufferSize: Vector2 = new Vector2() - public onBeforeRender: () => void = null - public onAfterRender: () => void = null + public onBeforeRender: (() => void) | undefined = undefined + public onAfterRender: (() => void) | undefined = undefined public constructor() { super() @@ -46,7 +50,7 @@ export class StencilPass extends BaseSpecklePass implements SpecklePass { public get displayName(): string { return 'STENCIL' } - public get outputTexture(): Texture { + public get outputTexture(): Texture | null { return null } @@ -54,7 +58,7 @@ export class StencilPass extends BaseSpecklePass implements SpecklePass { return this.overrideMaterial } - public update(scene: Scene, camera: Camera) { + public update(scene: Scene, camera: PerspectiveCamera | OrthographicCamera) { this.camera = camera this.scene = scene } @@ -63,12 +67,17 @@ export class StencilPass extends BaseSpecklePass implements SpecklePass { this.overrideMaterial.clippingPlanes = planes } - render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) { + render( + renderer: WebGLRenderer, + _writeBuffer: WebGLRenderTarget, + readBuffer: WebGLRenderTarget + ) { if (this.onBeforeRender) this.onBeforeRender() const oldAutoClear = renderer.autoClear renderer.autoClear = false - let oldClearAlpha, oldOverrideMaterial + let oldClearAlpha, + oldOverrideMaterial = null if (this.overrideMaterial !== undefined) { oldOverrideMaterial = this.scene.overrideMaterial diff --git a/packages/viewer/src/modules/queries/IntersectionQuerySolver.ts b/packages/viewer/src/modules/queries/IntersectionQuerySolver.ts index 83e1a9149b..48d9c19bdc 100644 --- a/packages/viewer/src/modules/queries/IntersectionQuerySolver.ts +++ b/packages/viewer/src/modules/queries/IntersectionQuerySolver.ts @@ -1,20 +1,20 @@ import Logger from 'js-logger' -import { Intersection, Ray, Vector2, Vector3 } from 'three' +import { type Intersection, Ray, Vector2, Vector3 } from 'three' import SpeckleRenderer from '../SpeckleRenderer' -import { IntersectionQuery, IntersectionQueryResult } from './Query' +import type { IntersectionQuery, IntersectionQueryResult } from './Query' import { ObjectLayers } from '../../IViewer' export class IntersectionQuerySolver { private vecBuff0: Vector3 = new Vector3() private vecBuff1: Vector3 = new Vector3() - private renderer: SpeckleRenderer + private renderer!: SpeckleRenderer public setContext(renderer: SpeckleRenderer) { this.renderer = renderer } - public solve(query: IntersectionQuery): IntersectionQueryResult { + public solve(query: IntersectionQuery): IntersectionQueryResult | null { switch (query.operation) { case 'Occlusion': return this.solveOcclusion(query) @@ -22,28 +22,31 @@ export class IntersectionQuerySolver { return this.solvePick(query) default: Logger.error('Malformed query') - break + return null } } private solveOcclusion(query: IntersectionQuery): IntersectionQueryResult { - const target = this.vecBuff0.set(query.point.x, query.point.y, query.point.z) + if (!this.renderer.renderingCamera) return { objects: null } + + const target = this.vecBuff0.set(query.point.x, query.point.y, query.point.z || 0) const dir = this.vecBuff1.copy(target).sub(this.renderer.renderingCamera.position) dir.normalize() const ray = new Ray(this.renderer.renderingCamera.position, dir) - const results: Array = this.renderer.intersections.intersectRay( - this.renderer.scene, - this.renderer.renderingCamera, - ray, - true, - this.renderer.clippingVolume, - [ObjectLayers.STREAM_CONTENT_MESH] - ) + const results: Array | null = + this.renderer.intersections.intersectRay( + this.renderer.scene, + this.renderer.renderingCamera, + ray, + ObjectLayers.STREAM_CONTENT_MESH, + true, + this.renderer.clippingVolume + ) if (!results || results.length === 0) return { objects: null } const hits = this.renderer.queryHitIds(results) if (!hits) return { objects: null } let targetDistance = this.renderer.renderingCamera.position.distanceTo(target) - targetDistance -= query.tolerance + targetDistance -= query.tolerance !== undefined ? query.tolerance : 0 if (targetDistance < results[0].distance) { return { objects: null } @@ -59,16 +62,20 @@ export class IntersectionQuerySolver { } } - private solvePick(query: IntersectionQuery): IntersectionQueryResult { - const results: Array = this.renderer.intersections.intersect( + private solvePick(query: IntersectionQuery): IntersectionQueryResult | null { + if (!this.renderer.renderingCamera) return null + + const results: Array | null = this.renderer.intersections.intersect( this.renderer.scene, this.renderer.renderingCamera, new Vector2(query.point.x, query.point.y), + undefined, true, this.renderer.clippingVolume ) if (!results) return null const hits = this.renderer.queryHits(results) + if (!hits) return null return { objects: hits.map((value) => { return { diff --git a/packages/viewer/src/modules/queries/PointQuerySolver.ts b/packages/viewer/src/modules/queries/PointQuerySolver.ts index 3593377115..2a246926e4 100644 --- a/packages/viewer/src/modules/queries/PointQuerySolver.ts +++ b/packages/viewer/src/modules/queries/PointQuerySolver.ts @@ -1,16 +1,16 @@ import Logger from 'js-logger' import { Vector3 } from 'three' import SpeckleRenderer from '../SpeckleRenderer' -import { PointQuery, PointQueryResult } from './Query' +import type { PointQuery, PointQueryResult } from './Query' export class PointQuerySolver { - private renderer: SpeckleRenderer + private renderer!: SpeckleRenderer public setContext(renderer: SpeckleRenderer) { this.renderer = renderer } - public solve(query: PointQuery): PointQueryResult { + public solve(query: PointQuery): PointQueryResult | null { switch (query.operation) { case 'Project': return this.solveProjection(query) @@ -18,14 +18,15 @@ export class PointQuerySolver { return this.solveUnprojection(query) default: Logger.error('Malformed query') - break + return null } } private solveProjection(query: PointQuery): PointQueryResult { // WORLD const projected = new Vector3(query.point.x, query.point.y, query.point.z) - projected.project(this.renderer.renderingCamera) + if (this.renderer.renderingCamera) projected.project(this.renderer.renderingCamera) + else Logger.error('Could not run query. Camera is null') return { // NDC @@ -38,8 +39,9 @@ export class PointQuerySolver { private solveUnprojection(query: PointQuery): PointQueryResult { // NDC const unprojected = new Vector3(query.point.x, query.point.y, query.point.z) - unprojected.unproject(this.renderer.renderingCamera) - + if (this.renderer.renderingCamera) + unprojected.unproject(this.renderer.renderingCamera) + else Logger.error('Could not run query. Camera is null') return { // WORLD x: unprojected.x, diff --git a/packages/viewer/src/modules/queries/Queries.ts b/packages/viewer/src/modules/queries/Queries.ts index 4f8ad5d101..bab8f0f966 100644 --- a/packages/viewer/src/modules/queries/Queries.ts +++ b/packages/viewer/src/modules/queries/Queries.ts @@ -1,6 +1,6 @@ import { IntersectionQuerySolver } from './IntersectionQuerySolver' import { PointQuerySolver } from './PointQuerySolver' -import { IntersectionQuery, PointQuery, Query } from './Query' +import type { IntersectionQuery, PointQuery, Query } from './Query' export class Queries { public static DefaultPointQuerySolver: PointQuerySolver = new PointQuerySolver() diff --git a/packages/viewer/src/modules/tree/DataTree.ts b/packages/viewer/src/modules/tree/DataTree.ts deleted file mode 100644 index e8d1c114eb..0000000000 --- a/packages/viewer/src/modules/tree/DataTree.ts +++ /dev/null @@ -1,71 +0,0 @@ -import TreeModel from 'tree-model' -import { TreeNode, WorldTree } from './WorldTree' - -export type SpeckleObject = Record -export type ObjectPredicate = (guid: string, obj: SpeckleObject) => boolean - -export interface DataTree { - findFirst(predicate: ObjectPredicate): SpeckleObject - findAll(predicate: ObjectPredicate): SpeckleObject[] - walk(predicate: ObjectPredicate): void -} - -class DataTreeInternal implements DataTree { - tree: TreeModel - root: TreeNode - - public constructor() { - this.tree = new TreeModel() - this.root = this.tree.parse({ guid: WorldTree.ROOT_ID }) - } - public findAll(predicate: ObjectPredicate): SpeckleObject[] { - return this.root - .all((node: TreeNode) => { - if (!node.model.data) return false - return predicate(node.model.guid, node.model.data) - }) - .map((value: TreeNode) => value.model.data) - } - - public findFirst(predicate: ObjectPredicate): SpeckleObject { - return this.root.first((node: TreeNode) => { - if (!node.model.data) return false - return predicate(node.model.guid, node.model.data) - }).model.data - } - - public walk(predicate: ObjectPredicate) { - this.root.walk((node: TreeNode) => { - if (!node.model.data) return true - return predicate(node.model.guid, node.model.data) - }) - } -} - -export class DataTreeBuilder { - public static build(tree: WorldTree): DataTree { - const dataTree = new DataTreeInternal() - let parent = null - tree.root.walk((node: TreeNode) => { - if (!node.parent) { - parent = dataTree.root - return true - } - - parent = dataTree.root.first((localNode) => { - return localNode.model.guid === node.parent.model.id - }) - - const _node: TreeNode = tree.parse({ - guid: node.model.id, - data: node.model.raw, - atomic: node.model.atomic, - children: [] - }) - parent.addChild(_node) - - return true - }, tree.root) - return dataTree as DataTree - } -} diff --git a/packages/viewer/src/modules/tree/NodeMap.ts b/packages/viewer/src/modules/tree/NodeMap.ts index 0579a7e04a..683b19d085 100644 --- a/packages/viewer/src/modules/tree/NodeMap.ts +++ b/packages/viewer/src/modules/tree/NodeMap.ts @@ -1,10 +1,9 @@ import Logger from 'js-logger' -import { TreeNode } from './WorldTree' +import { type TreeNode } from './WorldTree' export class NodeMap { public static readonly COMPOUND_ID_CHAR = '~' - private subtreeRoot: TreeNode private all: { [id: string]: TreeNode } = {} public instances: { [id: string]: { [id: string]: TreeNode } } = {} @@ -13,7 +12,6 @@ export class NodeMap { } public constructor(subtreeRoot: TreeNode) { - this.subtreeRoot = subtreeRoot this.registerNode(subtreeRoot) } @@ -43,7 +41,7 @@ export class NodeMap { return true } - public getNodeById(id: string): TreeNode[] { + public getNodeById(id: string): TreeNode[] | null { if (id.includes(NodeMap.COMPOUND_ID_CHAR)) { const baseId = id.substring(0, id.indexOf(NodeMap.COMPOUND_ID_CHAR)) if (this.instances[baseId]) { @@ -68,7 +66,7 @@ export class NodeMap { return this.all[id] } - public hasId(id: string) { + public hasId(id: string): boolean { if (id.includes(NodeMap.COMPOUND_ID_CHAR)) { const baseId = id.substring(0, id.indexOf(NodeMap.COMPOUND_ID_CHAR)) if (this.instances[baseId]) { @@ -83,9 +81,10 @@ export class NodeMap { if (this.instances[id]) { return true } + return false } - private registerInstance(node: TreeNode) { + private registerInstance(node: TreeNode): void { const baseId = node.model.id.substring( 0, node.model.id.indexOf(NodeMap.COMPOUND_ID_CHAR) @@ -101,7 +100,7 @@ export class NodeMap { } public purge() { - this.all = null - this.instances = null + this.all = {} + this.instances = {} } } diff --git a/packages/viewer/src/modules/tree/NodeRenderView.ts b/packages/viewer/src/modules/tree/NodeRenderView.ts index f5a176e12a..7a4cdb6afc 100644 --- a/packages/viewer/src/modules/tree/NodeRenderView.ts +++ b/packages/viewer/src/modules/tree/NodeRenderView.ts @@ -1,7 +1,10 @@ import { Box3 } from 'three' import { GeometryType } from '../batching/Batch' -import { GeometryData } from '../converter/Geometry' -import Materials, { DisplayStyle, RenderMaterial } from '../materials/Materials' +import { GeometryAttributes, type GeometryData } from '../converter/Geometry' +import Materials, { + type DisplayStyle, + type RenderMaterial +} from '../materials/Materials' import { SpeckleType } from '../loaders/GeometryConverter' export interface NodeRenderData { @@ -9,8 +12,8 @@ export interface NodeRenderData { subtreeId: number speckleType: SpeckleType geometry: GeometryData - renderMaterial: RenderMaterial - displayStyle: DisplayStyle + renderMaterial: RenderMaterial | null + displayStyle: DisplayStyle | null } export class NodeRenderView { @@ -23,22 +26,23 @@ export class NodeRenderView { private readonly _renderData: NodeRenderData private _materialHash: number private _geometryType: GeometryType - private _guid: string = null + private _guid: string | null = null - private _aabb: Box3 = null + private _aabb: Box3 - public get guid() { + /** TO DO: Not sure if we should store it */ + public get guid(): string { if (!this._guid) { this._guid = this._renderData.subtreeId + this._renderData.id } return this._guid } - public get renderData() { + public get renderData(): NodeRenderData { return this._renderData } - public get renderMaterialHash() { + public get renderMaterialHash(): number { return this._materialHash } @@ -50,15 +54,15 @@ export class NodeRenderView { return this._renderData.geometry && this._renderData.geometry.metaData } - public get speckleType() { + public get speckleType(): SpeckleType { return this._renderData.speckleType } - public get geometryType() { + public get geometryType(): GeometryType { return this._geometryType } - public get batchStart() { + public get batchStart(): number { return this._batchIndexStart } @@ -66,33 +70,35 @@ export class NodeRenderView { return this._batchIndexStart + this._batchIndexCount } - public get batchCount() { + public get batchCount(): number { return this._batchIndexCount } - public get batchId() { + public get batchId(): string { return this._batchId } - public get aabb() { + public get aabb(): Box3 { return this._aabb } - public get transparent() { + public get transparent(): boolean { return ( - this._renderData.renderMaterial && this._renderData.renderMaterial.opacity < 1 + (this._renderData.renderMaterial && + this._renderData.renderMaterial.opacity < 1) || + false ) } - public get vertStart() { + public get vertStart(): number { return this._batchVertexStart } - public get vertEnd() { + public get vertEnd(): number { return this._batchVertexEnd } - public get needsSegmentConversion() { + public get needsSegmentConversion(): boolean { return ( this._renderData.speckleType === SpeckleType.Curve || this._renderData.speckleType === SpeckleType.Polyline || @@ -103,15 +109,16 @@ export class NodeRenderView { ) } - public get validGeometry() { + public get validGeometry(): boolean { return ( - this._renderData.geometry.attributes && - this._renderData.geometry.attributes.POSITION && - this._renderData.geometry.attributes.POSITION.length > 0 && - (this._geometryType === GeometryType.MESH - ? this._renderData.geometry.attributes.INDEX && - this._renderData.geometry.attributes.INDEX.length > 0 - : true) + (this._renderData.geometry.attributes && + this._renderData.geometry.attributes.POSITION && + this._renderData.geometry.attributes.POSITION.length > 0 && + (this._geometryType === GeometryType.MESH + ? this._renderData.geometry.attributes.INDEX && + this._renderData.geometry.attributes.INDEX.length > 0 + : true)) || + false ) } @@ -120,11 +127,11 @@ export class NodeRenderView { this._geometryType = this.getGeometryType() this._materialHash = Materials.getMaterialHash(this) - this._batchId - this._batchIndexCount - this._batchIndexStart - this._batchVertexStart - this._batchVertexEnd + this._batchId = '' + this._batchIndexCount = 0 + this._batchIndexStart = -1 + this._batchVertexStart = -1 + this._batchVertexEnd = -1 } public setBatchData( @@ -142,10 +149,12 @@ export class NodeRenderView { } public computeAABB() { - this._aabb = new Box3().setFromArray(this._renderData.geometry.attributes.POSITION) + this._aabb = new Box3() + if (this._renderData.geometry.attributes) + this._aabb.setFromArray(this._renderData.geometry.attributes.POSITION) } - public getGeometryType(): GeometryType { + private getGeometryType(): GeometryType { switch (this._renderData.speckleType) { case SpeckleType.Mesh: return GeometryType.MESH @@ -165,7 +174,7 @@ export class NodeRenderView { public disposeGeometry() { for (const attr in this._renderData.geometry.attributes) { - this._renderData.geometry.attributes[attr] = [] + this._renderData.geometry.attributes[attr as GeometryAttributes] = [] } } } diff --git a/packages/viewer/src/modules/tree/RenderTree.ts b/packages/viewer/src/modules/tree/RenderTree.ts index 0d5f0b79b8..7e9ae365d2 100644 --- a/packages/viewer/src/modules/tree/RenderTree.ts +++ b/packages/viewer/src/modules/tree/RenderTree.ts @@ -1,7 +1,7 @@ -import { Box3, Matrix4 } from 'three' -import { TreeNode, WorldTree } from './WorldTree' +import { Matrix4 } from 'three' +import { type TreeNode, WorldTree } from './WorldTree' import Materials from '../materials/Materials' -import { NodeRenderData, NodeRenderView } from './NodeRenderView' +import { type NodeRenderData, NodeRenderView } from './NodeRenderView' import Logger from 'js-logger' import { GeometryConverter, SpeckleType } from '../loaders/GeometryConverter' import { Geometry } from '../converter/Geometry' @@ -9,13 +9,8 @@ import { Geometry } from '../converter/Geometry' export class RenderTree { private tree: WorldTree private root: TreeNode - private _treeBounds: Box3 = new Box3() private cancel = false - public get treeBounds(): Box3 { - return this._treeBounds - } - public get id(): string { return this.root.model.id } @@ -55,7 +50,6 @@ export class RenderTree { ) } node.model.renderView.computeAABB() - this._treeBounds.union(node.model.renderView.aabb) } else if (node.model.renderView.hasMetadata) { node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform) } @@ -65,8 +59,8 @@ export class RenderTree { private buildRenderNode( node: TreeNode, geometryConverter: GeometryConverter - ): NodeRenderData { - let ret: NodeRenderData = null + ): NodeRenderData | null { + let ret: NodeRenderData | null = null const geometryData = geometryConverter.convertNodeToGeometryData(node.model) if (geometryData) { const renderMaterialNode = this.getRenderMaterialNode(node) @@ -89,7 +83,7 @@ export class RenderTree { return ret } - private getRenderMaterialNode(node: TreeNode): TreeNode { + private getRenderMaterialNode(node: TreeNode): TreeNode | null { if (node.model.raw.renderMaterial) { return node } @@ -99,9 +93,10 @@ export class RenderTree { return ancestors[k] } } + return null } - private getDisplayStyleNode(node: TreeNode): TreeNode { + private getDisplayStyleNode(node: TreeNode): TreeNode | null { if (node.model.raw.displayStyle) { return node } @@ -111,6 +106,7 @@ export class RenderTree { return ancestors[k] } } + return null } public computeTransform(node: TreeNode): Matrix4 { @@ -123,7 +119,10 @@ export class RenderTree { for (let k = 0; k < ancestors.length; k++) { if (ancestors[k].model.renderView) { const renderNode: NodeRenderData = ancestors[k].model.renderView.renderData - if (renderNode.speckleType === SpeckleType.Transform) { + if ( + renderNode.speckleType === SpeckleType.Transform && + renderNode.geometry.transform + ) { transform.premultiply(renderNode.geometry.transform) } } @@ -151,41 +150,26 @@ export class RenderTree { }) } - /** This gets the render views for a particular node/id. - * Currently it doesn't treat Blocks in a special way, but - * we might want to. - */ - public getRenderViewsForNode(node: TreeNode, parent?: TreeNode): NodeRenderView[] { - if ( - node.model.atomic && - node.model.renderView && - node.model.renderView.renderData.speckleType !== SpeckleType.RevitInstance && - node.model.renderView.renderData.speckleType !== SpeckleType.BlockInstance - ) { - return [node.model.renderView] - } - - return (parent ? parent : node.parent) - .all((_node: TreeNode): boolean => { - return ( - _node.model.renderView && - (_node.model.renderView.hasGeometry || _node.model.renderView.hasMetadata) - ) - }) - .map((val: TreeNode) => val.model.renderView) + public getRenderViewsForNode(node: TreeNode): NodeRenderView[] { + return this.getRenderViewNodesForNode(node).map( + (val: TreeNode) => val.model.renderView + ) } - public getRenderViewNodesForNode(node: TreeNode, parent?: TreeNode): TreeNode[] { + public getRenderViewNodesForNode(node: TreeNode): TreeNode[] { if ( node.model.atomic && - node.model.renderView && + node.model.renderView + /** This should not be needed anymore. */ + /*&& node.model.renderView.renderData.speckleType !== SpeckleType.RevitInstance && node.model.renderView.renderData.speckleType !== SpeckleType.BlockInstance + */ ) { return [node] } - return (parent ? parent : node.parent).all((_node: TreeNode): boolean => { + return node.all((_node: TreeNode): boolean => { return ( _node.model.renderView && (_node.model.renderView.hasGeometry || _node.model.renderView.hasMetadata) @@ -193,45 +177,33 @@ export class RenderTree { }) } - public getAtomicParent(node: TreeNode) { - if (node.model.atomic) { - return node - } - return this.tree.getAncestors(node).find((node) => node.model.atomic) - } - - public getRenderViewsForNodeId(id: string): NodeRenderView[] { + public getRenderViewsForNodeId(id: string): NodeRenderView[] | null { const nodes = this.tree.findId(id) if (!nodes) { Logger.warn(`Id ${id} does not exist`) return null } - const ret = [] + const ret: Array = [] nodes.forEach((node: TreeNode) => { - ret.push(...this.getRenderViewsForNode(node, node)) + ret.push(...this.getRenderViewsForNode(node)) }) return ret } - public getRenderViewForNodeId(id: string): NodeRenderView { - const nodes = this.tree.findId(id) - if (!nodes) { - Logger.warn(`Id ${id} does not exist`) - return null - } - if (nodes.length > 1) { - Logger.warn(`Multiple nodes with ${id} found. Returning first only`) + public getAtomicParent(node: TreeNode): TreeNode { + if (node.model.atomic) { + return node } - return nodes[0].model.renderView + /** There will always the root of the tree as the atomic parent for all nodes */ + return this.tree.getAncestors(node).find((node) => node.model.atomic) as TreeNode } - public purge() { - this.tree = null - } + public purge() {} - public cancelBuild(subtreeId: string) { + /** TO DO: Need to purge only if currently building */ + public cancelBuild(): void { this.cancel = true - this.tree.purge(subtreeId) + this.tree.purge(this.id) this.purge() } } diff --git a/packages/viewer/src/modules/tree/WorldTree.ts b/packages/viewer/src/modules/tree/WorldTree.ts index 7c0bd2a990..54b3f0be0a 100644 --- a/packages/viewer/src/modules/tree/WorldTree.ts +++ b/packages/viewer/src/modules/tree/WorldTree.ts @@ -1,4 +1,4 @@ -import TreeModel from 'tree-model' +import TreeModel, { type Model } from 'tree-model' import { NodeRenderView } from './NodeRenderView' import { RenderTree } from './RenderTree' import Logger from 'js-logger' @@ -7,22 +7,22 @@ import { NodeMap } from './NodeMap' export type TreeNode = TreeModel.Node export type SearchPredicate = (node: TreeNode) => boolean -export type AsyncSearchPredicate = (node: TreeNode) => Promise export interface NodeData { + id: string // eslint-disable-next-line @typescript-eslint/no-explicit-any raw: { [prop: string]: any } children: TreeNode[] - nestedNodes: TreeNode[] atomic: boolean + nestedNodes?: TreeNode[] subtreeId?: number - renderView?: NodeRenderView + renderView?: NodeRenderView | null instanced?: boolean } export class WorldTree { private renderTreeInstances: { [id: string]: RenderTree } = {} - public nodeMaps: { [id: string]: NodeMap } = {} + private nodeMaps: { [id: string]: NodeMap } = {} private readonly supressWarnings = true public static readonly ROOT_ID = 'ROOT' private subtreeId: number = 0 @@ -38,7 +38,10 @@ export class WorldTree { }) } - public getRenderTree(subtreeId?: string): RenderTree { + /** The root render tree will always be non-null because it will always contain the root */ + public getRenderTree(): RenderTree + public getRenderTree(subtreeId: string): RenderTree | null + public getRenderTree(subtreeId?: string): RenderTree | null { if (!this._root) { console.error(`WorldTree not initialised`) return null @@ -63,17 +66,17 @@ export class WorldTree { return this._root } - public get nextSubtreeId(): number { + private get nextSubtreeId(): number { return ++this.subtreeId } - public get nodeCount() { + public get nodeCount(): number { let nodeCount = 0 for (const k in this.nodeMaps) nodeCount += this.nodeMaps[k].nodeCount return nodeCount } - public isRoot(node: TreeNode) { + public isRoot(node: TreeNode): boolean { return node === this._root } @@ -81,7 +84,7 @@ export class WorldTree { return node.parent === this._root } - public parse(model) { + public parse(model: Model): TreeNode { return this.tree.parse(model) } @@ -96,7 +99,7 @@ export class WorldTree { this._root.addChild(node) } - public addNode(node: TreeNode, parent: TreeNode) { + public addNode(node: TreeNode, parent: TreeNode | null) { if (parent === null || parent.model.subtreeId === undefined) { Logger.error(`Invalid parent node!`) return @@ -105,7 +108,7 @@ export class WorldTree { if (this.nodeMaps[parent.model.subtreeId]?.addNode(node)) parent.addChild(node) } - public removeNode(node: TreeNode) { + public removeNode(node: TreeNode): void { node.drop() } @@ -116,7 +119,7 @@ export class WorldTree { return (node ? node : this.root).all(predicate) } - public findId(id: string, subtreeId?: number) { + public findId(id: string, subtreeId?: number): TreeNode[] | null { let idNode = null if (subtreeId) { idNode = this.nodeMaps[subtreeId].getNodeById(id) @@ -129,6 +132,7 @@ export class WorldTree { return idNode } + /** TODO: Would rather not have this */ public findSubtree(id: string) { let idNode = null for (const k in this.nodeMaps) { @@ -141,10 +145,11 @@ export class WorldTree { return node.getPath().reverse().slice(1) // We skip the node itself } - public getInstances(subtree: string): { [id: string]: Record } { - return this.nodeMaps[subtree].instances + public getInstances(subtreeId: string): { [id: string]: Record } { + return this.nodeMaps[subtreeId].instances } + /** TO DO: We might want to add boolean as return type here too */ public walk(predicate: SearchPredicate, node?: TreeNode): void { if (!node && !this.supressWarnings) { Logger.warn(`Root will be used for searching. You might not want that`) @@ -162,7 +167,10 @@ export class WorldTree { const pause = new AsyncPause() let success = true - async function depthFirstPreOrderAsync(callback, context) { + async function depthFirstPreOrderAsync( + callback: SearchPredicate, + context: TreeNode + ) { let i, childCount pause.tick(100) if (pause.needsWait) { @@ -183,10 +191,12 @@ export class WorldTree { public purge(subtreeId?: string) { if (subtreeId) { delete this.renderTreeInstances[subtreeId] - const subtreeNode = this.findId(subtreeId)[0] - this.nodeMaps[subtreeNode.model.subtreeId].purge() - delete this.nodeMaps[subtreeNode.model.subtreeId] - this.removeNode(subtreeNode) + const subtreeNode = this.findId(subtreeId) + if (subtreeNode) { + this.nodeMaps[subtreeNode[0].model.subtreeId].purge() + delete this.nodeMaps[subtreeNode[0].model.subtreeId] + this.removeNode(subtreeNode[0]) + } return } diff --git a/packages/viewer/tsconfig.json b/packages/viewer/tsconfig.json index a1cd799893..74f2229df7 100644 --- a/packages/viewer/tsconfig.json +++ b/packages/viewer/tsconfig.json @@ -4,13 +4,16 @@ "lib": ["DOM"], "module": "es2020", "moduleResolution": "Bundler", - "strict": false, + "strict": true, "sourceMap": true, "isolatedModules": true, "esModuleInterop": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noImplicitReturns": false, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + "verbatimModuleSyntax": false, + "strictPropertyInitialization": false /* We are not building a ToDO list app. This option is ridiculous */, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, "skipLibCheck": true, "outDir": "./dist", "allowJs": true, diff --git a/utils/docker-compose-ingress/Dockerfile b/utils/docker-compose-ingress/Dockerfile index 4db671d571..65941a9087 100644 --- a/utils/docker-compose-ingress/Dockerfile +++ b/utils/docker-compose-ingress/Dockerfile @@ -5,4 +5,4 @@ RUN mkdir -p /var/nginx COPY utils/docker-compose-ingress/nginx/templates /etc/nginx/templates COPY utils/docker-compose-ingress/nginx/conf/mime.types /etc/nginx/mime.types -EXPOSE 8080 +EXPOSE 8080 \ No newline at end of file diff --git a/utils/docker-compose-ingress/nginx/default.conf b/utils/docker-compose-ingress/nginx/default.conf new file mode 100644 index 0000000000..17b498bf43 --- /dev/null +++ b/utils/docker-compose-ingress/nginx/default.conf @@ -0,0 +1,27 @@ +server { + listen 8080; + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://127.0.0.1:8081; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + location ~* ^/(graphql|explorer|(auth/.*)|(objects/.*)|(preview/.*)|(api/.*)|(static/.*)) { + resolver 127.0.0.11 valid=30s; + set $upstream_speckle_server speckle-server; + client_max_body_size 300m; + proxy_pass http://127.0.0.1:3000; + + proxy_buffering off; + proxy_request_buffering off; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} diff --git a/utils/helm/speckle-server/templates/frontend_2/deployment.yml b/utils/helm/speckle-server/templates/frontend_2/deployment.yml index 0ad3b98fcb..e961dc7bdc 100644 --- a/utils/helm/speckle-server/templates/frontend_2/deployment.yml +++ b/utils/helm/speckle-server/templates/frontend_2/deployment.yml @@ -125,6 +125,8 @@ spec: - name: NUXT_PUBLIC_DATADOG_ENV value: {{ .Values.analytics.datadog_env | quote }} {{- end }} + - name: FF_AUTOMATE_MODULE_ENABLED + value: {{ .Values.featureFlags.automateModuleEnabled | quote }} {{- if .Values.analytics.survicate_workspace_key }} - name: NUXT_PUBLIC_SURVICATE_WORKSPACE_KEY value: {{ .Values.analytics.survicate_workspace_key | quote }} diff --git a/utils/helm/speckle-server/templates/server/deployment.yml b/utils/helm/speckle-server/templates/server/deployment.yml index 3d1e02a33d..80d10b5752 100644 --- a/utils/helm/speckle-server/templates/server/deployment.yml +++ b/utils/helm/speckle-server/templates/server/deployment.yml @@ -111,6 +111,9 @@ spec: - name: ENABLE_FE2_MESSAGING value: {{ .Values.server.enableFe2Messaging | quote }} + - name: FF_AUTOMATE_MODULE_ENABLED + value: {{ .Values.featureFlags.automateModuleEnabled | quote }} + - name: ONBOARDING_STREAM_URL value: {{ .Values.server.onboarding.stream_url }} - name: ONBOARDING_STREAM_CACHE_BUST_NUMBER diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index bd1672c952..db1dfef4e5 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -32,6 +32,16 @@ "description": "The name of the ClusterIssuer kubernetes resource that provides the SSL Certificate", "default": "letsencrypt-staging" }, + "featureFlags": { + "type": "object", + "properties": { + "automateModuleEnabled": { + "type": "boolean", + "description": "High level flag fully toggles the integrated automate module", + "default": false + } + } + }, "analytics": { "type": "object", "properties": { diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index cefb616211..648ad40c94 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -30,6 +30,14 @@ tlsRejectUnauthorized: '1' ## cert_manager_issuer: letsencrypt-staging +## @section Feature flags +## @descriptionStart +## This object is a central location to define feature flags for the whole chart. +## @descriptionEnd +featureFlags: + ## @param featureFlags.automateModuleEnabled High level flag fully toggles the integrated automate module + automateModuleEnabled: false + analytics: ## @param analytics.enabled Enable or disable analytics enabled: true diff --git a/workspace.code-workspace b/workspace.code-workspace index f59af2f5ab..ae9f223619 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -113,7 +113,7 @@ }, "vue.complete.casing.props": "kebab", "vue.inlayHints.missingProps": true, - "circleci.persistedProjectSelection": [] + "circleci.persistedProjectSelection": ["gh/specklesystems/speckle-server"] }, "extensions": { // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. @@ -128,7 +128,9 @@ "Vue.volar", "bradlc.vscode-tailwindcss", "stylelint.vscode-stylelint", - "cpylua.language-postcss" + "cpylua.language-postcss", + "graphql.vscode-graphql", + "graphql.vscode-graphql-syntax" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": ["octref.vetur", "vscode.typescript-language-features"] diff --git a/yarn.lock b/yarn.lock index 0db3c0a553..d2c474f22a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10474,7 +10474,14 @@ __metadata: languageName: node linkType: hard -"@ioredis/commands@npm:^1.1.1": +"@ioredis/as-callback@npm:^3.0.0": + version: 3.0.0 + resolution: "@ioredis/as-callback@npm:3.0.0" + checksum: 2835e39631497fe4f8b07d95576abea165c9f7efef81e9e55c733588051ff4edcb31eeb59f36127923dae0cb1a8e21b4e27ee3ab79a065a0baeecf861c3bc0b1 + languageName: node + linkType: hard + +"@ioredis/commands@npm:^1.1.1, @ioredis/commands@npm:^1.2.0": version: 1.2.0 resolution: "@ioredis/commands@npm:1.2.0" checksum: 9b20225ba36ef3e5caf69b3c0720597c3016cc9b1e157f519ea388f621dd9037177f84cfe7e25c4c32dad7dd90c70ff9123cd411f747e053cf292193c9c461e2 @@ -12349,305 +12356,6 @@ __metadata: languageName: node linkType: hard -"@octokit/app@npm:^14.0.2": - version: 14.1.0 - resolution: "@octokit/app@npm:14.1.0" - dependencies: - "@octokit/auth-app": ^6.0.0 - "@octokit/auth-unauthenticated": ^5.0.0 - "@octokit/core": ^5.0.0 - "@octokit/oauth-app": ^6.0.0 - "@octokit/plugin-paginate-rest": ^9.0.0 - "@octokit/types": ^12.0.0 - "@octokit/webhooks": ^12.0.4 - checksum: 2a27ea831d0367b07f3c4109bbc840c7ae7d5a52d3129593cd867364794eb51b16b0fc308b116a89af9a2f19553c72346e03dd07b952e82c222ed1e7880dfcac - languageName: node - linkType: hard - -"@octokit/auth-app@npm:^6.0.0": - version: 6.1.1 - resolution: "@octokit/auth-app@npm:6.1.1" - dependencies: - "@octokit/auth-oauth-app": ^7.1.0 - "@octokit/auth-oauth-user": ^4.1.0 - "@octokit/request": ^8.3.1 - "@octokit/request-error": ^5.1.0 - "@octokit/types": ^13.1.0 - deprecation: ^2.3.1 - lru-cache: ^10.0.0 - universal-github-app-jwt: ^1.1.2 - universal-user-agent: ^6.0.0 - checksum: 6b3b299865f4a612cf308b6c01ba38101930d1e3eb3444c4eaa5365bec9d62538d45b471e1ee3677244e26b899316bd4ad30ade821564f7f48ff9f51bb74c423 - languageName: node - linkType: hard - -"@octokit/auth-oauth-app@npm:^7.0.0, @octokit/auth-oauth-app@npm:^7.1.0": - version: 7.1.0 - resolution: "@octokit/auth-oauth-app@npm:7.1.0" - dependencies: - "@octokit/auth-oauth-device": ^6.1.0 - "@octokit/auth-oauth-user": ^4.1.0 - "@octokit/request": ^8.3.1 - "@octokit/types": ^13.0.0 - "@types/btoa-lite": ^1.0.0 - btoa-lite: ^1.0.0 - universal-user-agent: ^6.0.0 - checksum: 021e13c138279e9edd7d6dcdc484a2658ae07b834ec3f5f41158e3870b3413deb09024408d1615731c960243ba710ca638a868dcd2583f7eb80fa6204b70657b - languageName: node - linkType: hard - -"@octokit/auth-oauth-device@npm:^6.1.0": - version: 6.1.0 - resolution: "@octokit/auth-oauth-device@npm:6.1.0" - dependencies: - "@octokit/oauth-methods": ^4.1.0 - "@octokit/request": ^8.3.1 - "@octokit/types": ^13.0.0 - universal-user-agent: ^6.0.0 - checksum: 2824f74ea5eca3d8da9793f463ebca725c8a13a241085015f96f037771ef3e5fa82d5842f538353c683b709d8d32ccd481bfc0ba8cbcde708916ea95a78dd0d2 - languageName: node - linkType: hard - -"@octokit/auth-oauth-user@npm:^4.0.0, @octokit/auth-oauth-user@npm:^4.1.0": - version: 4.1.0 - resolution: "@octokit/auth-oauth-user@npm:4.1.0" - dependencies: - "@octokit/auth-oauth-device": ^6.1.0 - "@octokit/oauth-methods": ^4.1.0 - "@octokit/request": ^8.3.1 - "@octokit/types": ^13.0.0 - btoa-lite: ^1.0.0 - universal-user-agent: ^6.0.0 - checksum: 581197a427c1ef153350e46de7315c9da1a98904b67e5e13aed88d36e334d95d869f8f12a35ed70d7232c6afd6d3912200988e41959e30c83f880d072ee8b8ba - languageName: node - linkType: hard - -"@octokit/auth-token@npm:^4.0.0": - version: 4.0.0 - resolution: "@octokit/auth-token@npm:4.0.0" - checksum: d78f4dc48b214d374aeb39caec4fdbf5c1e4fd8b9fcb18f630b1fe2cbd5a880fca05445f32b4561f41262cb551746aeb0b49e89c95c6dd99299706684d0cae2f - languageName: node - linkType: hard - -"@octokit/auth-unauthenticated@npm:^5.0.0": - version: 5.0.1 - resolution: "@octokit/auth-unauthenticated@npm:5.0.1" - dependencies: - "@octokit/request-error": ^5.0.0 - "@octokit/types": ^12.0.0 - checksum: b6eed1fc15d47f45411c0229dd6613dd8fd4b79afbac23b8c47818da692a35d54f57e088294d9b71ce4dcc0f58ce0c77d12cd2700370d87770059248b9a8fbba - languageName: node - linkType: hard - -"@octokit/core@npm:^5.0.0": - version: 5.2.0 - resolution: "@octokit/core@npm:5.2.0" - dependencies: - "@octokit/auth-token": ^4.0.0 - "@octokit/graphql": ^7.1.0 - "@octokit/request": ^8.3.1 - "@octokit/request-error": ^5.1.0 - "@octokit/types": ^13.0.0 - before-after-hook: ^2.2.0 - universal-user-agent: ^6.0.0 - checksum: 57d5f02b759b569323dcb76cc72bf94ea7d0de58638c118ee14ec3e37d303c505893137dd72918328794844f35c74b3cd16999319c4b40d410a310d44a9b7566 - languageName: node - linkType: hard - -"@octokit/endpoint@npm:^9.0.1": - version: 9.0.5 - resolution: "@octokit/endpoint@npm:9.0.5" - dependencies: - "@octokit/types": ^13.1.0 - universal-user-agent: ^6.0.0 - checksum: d5cc2df9bd4603844c163eea05eec89c677cfe699c6f065fe86b83123e34554ec16d429e8142dec1e2b4cf56591ef0ce5b1763f250c87bc8e7bf6c74ba59ae82 - languageName: node - linkType: hard - -"@octokit/graphql@npm:^7.1.0": - version: 7.1.0 - resolution: "@octokit/graphql@npm:7.1.0" - dependencies: - "@octokit/request": ^8.3.0 - "@octokit/types": ^13.0.0 - universal-user-agent: ^6.0.0 - checksum: 7b2706796e0269fc033ed149ea211117bcacf53115fd142c1eeafc06ebc5b6290e4e48c03d6276c210d72e3695e8598f83caac556cd00714fc1f8e4707d77448 - languageName: node - linkType: hard - -"@octokit/oauth-app@npm:^6.0.0": - version: 6.1.0 - resolution: "@octokit/oauth-app@npm:6.1.0" - dependencies: - "@octokit/auth-oauth-app": ^7.0.0 - "@octokit/auth-oauth-user": ^4.0.0 - "@octokit/auth-unauthenticated": ^5.0.0 - "@octokit/core": ^5.0.0 - "@octokit/oauth-authorization-url": ^6.0.2 - "@octokit/oauth-methods": ^4.0.0 - "@types/aws-lambda": ^8.10.83 - universal-user-agent: ^6.0.0 - checksum: 4759ef41624928efee484802e3a6280d7a92205f435e0d299bc4b1e39661427d7f9ec33ef0d752dd6ee665e37d4afa81c8a6aea10ba53b8eb7da66167b0c52d4 - languageName: node - linkType: hard - -"@octokit/oauth-authorization-url@npm:^6.0.2": - version: 6.0.2 - resolution: "@octokit/oauth-authorization-url@npm:6.0.2" - checksum: 0f11169a3eeb782cc08312c923de1a702b25ae033b972ba40380b6d72cb3f684543c8b6a5cf6f05936fdc6b8892070d4f7581138d8efc1b4c4a55ae6d7762327 - languageName: node - linkType: hard - -"@octokit/oauth-methods@npm:^4.0.0, @octokit/oauth-methods@npm:^4.1.0": - version: 4.1.0 - resolution: "@octokit/oauth-methods@npm:4.1.0" - dependencies: - "@octokit/oauth-authorization-url": ^6.0.2 - "@octokit/request": ^8.3.1 - "@octokit/request-error": ^5.1.0 - "@octokit/types": ^13.0.0 - btoa-lite: ^1.0.0 - checksum: 2ca42f054a3b92f6f3fa9a984df7d75cc8c1f19aba5f6fc9636499dde3a8031e33148cbc936cace103b1eb7fe79d978aee7077aa6f69e0dd996ee345a10f2aa4 - languageName: node - linkType: hard - -"@octokit/openapi-types@npm:^20.0.0": - version: 20.0.0 - resolution: "@octokit/openapi-types@npm:20.0.0" - checksum: 23ff7613750f8b5790a0cbed5a2048728a7909e50d726932831044908357a932c7fc0613fb7b86430a49d31b3d03a180632ea5dd936535bfbc1176391a199e96 - languageName: node - linkType: hard - -"@octokit/openapi-types@npm:^22.0.1": - version: 22.0.1 - resolution: "@octokit/openapi-types@npm:22.0.1" - checksum: f361764bf965081bb94facc33a171a98c4d94285e5c218ca6355a5aea35d1ec732ab7fa7ac941034ca249601d768cfa5205bcbef0980c0c6faa2b842efeed2ec - languageName: node - linkType: hard - -"@octokit/plugin-paginate-graphql@npm:^4.0.0": - version: 4.0.1 - resolution: "@octokit/plugin-paginate-graphql@npm:4.0.1" - peerDependencies: - "@octokit/core": ">=5" - checksum: 109d895303d39c1ba362a260c71202f3c92798faa4f4e05638023685b5ac9191cee61759ea0eee43b9ce945cf8c52aebf2dbd54c392165e86448d6421e97b0f5 - languageName: node - linkType: hard - -"@octokit/plugin-paginate-rest@npm:^9.0.0": - version: 9.2.1 - resolution: "@octokit/plugin-paginate-rest@npm:9.2.1" - dependencies: - "@octokit/types": ^12.6.0 - peerDependencies: - "@octokit/core": 5 - checksum: 554ad17a7dcfd7028e321ffcae233f8ae7975569084f19d9b6217b47fb182e2604145108de7a9029777e6dc976b27b2dd7387e2e47a77532a72e6c195880576d - languageName: node - linkType: hard - -"@octokit/plugin-rest-endpoint-methods@npm:^10.0.0": - version: 10.4.1 - resolution: "@octokit/plugin-rest-endpoint-methods@npm:10.4.1" - dependencies: - "@octokit/types": ^12.6.0 - peerDependencies: - "@octokit/core": 5 - checksum: 3e0e95515ccb7fdd5e5cff32a5e34a688fd275c6703caf786f7c49820e2bf2a66e7d845ba4eae4d03c307c1950ea417e34a17055b25b46e2019123b75b394c56 - languageName: node - linkType: hard - -"@octokit/plugin-retry@npm:^6.0.0": - version: 6.0.1 - resolution: "@octokit/plugin-retry@npm:6.0.1" - dependencies: - "@octokit/request-error": ^5.0.0 - "@octokit/types": ^12.0.0 - bottleneck: ^2.15.3 - peerDependencies: - "@octokit/core": ">=5" - checksum: 9c8663b5257cf4fa04cc737c064e9557501719d6d3af7cf8f46434a2117e1cf4b8d25d9eb4294ed255ad17a0ede853542649870612733f4b8ece97e24e391d22 - languageName: node - linkType: hard - -"@octokit/plugin-throttling@npm:^8.0.0": - version: 8.2.0 - resolution: "@octokit/plugin-throttling@npm:8.2.0" - dependencies: - "@octokit/types": ^12.2.0 - bottleneck: ^2.15.3 - peerDependencies: - "@octokit/core": ^5.0.0 - checksum: 12c357175783bcd0feea454ece57f033928948a0555dc97c79675b56d2cc79043d2a5e28a7554d3531f1de13583634df3b48fb9609f79e8bb3adad92820bd807 - languageName: node - linkType: hard - -"@octokit/request-error@npm:^5.0.0, @octokit/request-error@npm:^5.1.0": - version: 5.1.0 - resolution: "@octokit/request-error@npm:5.1.0" - dependencies: - "@octokit/types": ^13.1.0 - deprecation: ^2.0.0 - once: ^1.4.0 - checksum: 2cdbb8e44072323b5e1c8c385727af6700e3e492d55bc1e8d0549c4a3d9026914f915866323d371b1f1772326d6e902341c872679cc05c417ffc15cadf5f4a4e - languageName: node - linkType: hard - -"@octokit/request@npm:^8.3.0, @octokit/request@npm:^8.3.1": - version: 8.4.0 - resolution: "@octokit/request@npm:8.4.0" - dependencies: - "@octokit/endpoint": ^9.0.1 - "@octokit/request-error": ^5.1.0 - "@octokit/types": ^13.1.0 - universal-user-agent: ^6.0.0 - checksum: 3d937e817a85c0adf447ab46b428ccd702c31b2091e47adec90583ec2242bd64666306fe8188628fb139aa4752e19400eb7652b0f5ca33cd9e77bbb2c60b202a - languageName: node - linkType: hard - -"@octokit/types@npm:^12.0.0, @octokit/types@npm:^12.2.0, @octokit/types@npm:^12.6.0": - version: 12.6.0 - resolution: "@octokit/types@npm:12.6.0" - dependencies: - "@octokit/openapi-types": ^20.0.0 - checksum: 850235f425584499a2266d5c585c1c2462ae11e25c650567142f3342cb9ce589c8c8fed87705811ca93271fd28c68e1fa77b88b67b97015d7b63d269fa46ed05 - languageName: node - linkType: hard - -"@octokit/types@npm:^13.0.0, @octokit/types@npm:^13.1.0": - version: 13.4.0 - resolution: "@octokit/types@npm:13.4.0" - dependencies: - "@octokit/openapi-types": ^22.0.1 - checksum: 71d1e61e82ca10cb7f9e79d15158b7a9e84a6fe815fa865e7adbafee225963a18919316d7f0d7c96bc9e6751cf0f1663da056da468e8d5cebcbdaf44316cafba - languageName: node - linkType: hard - -"@octokit/webhooks-methods@npm:^4.1.0": - version: 4.1.0 - resolution: "@octokit/webhooks-methods@npm:4.1.0" - checksum: 0ce67220156d554ae4bc6a7230ae62c0389b9bbee1f6d1077947e64645ee864f0702778e86427d59ae970176620753f54edb44665cedbeb9bc22b9348a074427 - languageName: node - linkType: hard - -"@octokit/webhooks-types@npm:7.4.0": - version: 7.4.0 - resolution: "@octokit/webhooks-types@npm:7.4.0" - checksum: bedb819a6ad944ea95cab56da69a0c158d5f689d7f24a45e9a45bcbc4a34550858b1ef0d80a5f4c2fe02a6fc8d14302ca07123fc16a7cce93bb175c11f6a68dc - languageName: node - linkType: hard - -"@octokit/webhooks@npm:^12.0.4": - version: 12.2.0 - resolution: "@octokit/webhooks@npm:12.2.0" - dependencies: - "@octokit/request-error": ^5.0.0 - "@octokit/webhooks-methods": ^4.1.0 - "@octokit/webhooks-types": 7.4.0 - aggregate-error: ^3.1.0 - checksum: 69d32fd24ea00f632d1ba3edb84c8e15852b47ad120fe7db938bc8fd1f2823dd7e61707b3280a29818925871b51e472c5f892f76eee0c6d0cee8d0e51c7b5f5d - languageName: node - linkType: hard - "@open-draft/until@npm:^1.0.3": version: 1.0.3 resolution: "@open-draft/until@npm:1.0.3" @@ -14100,6 +13808,8 @@ __metadata: stylelint-config-standard: ^26.0.0 subscriptions-transport-ws: ^0.11.0 tailwindcss: ^3.4.1 + tweetnacl-sealedbox-js: ^1.2.0 + tweetnacl-util: ^0.15.1 type-fest: ^3.5.1 typescript: ^4.8.3 vee-validate: ^4.7.0 @@ -14299,12 +14009,14 @@ __metadata: "@tiptap/core": ^2.0.0-beta.176 "@types/bcrypt": ^5.0.0 "@types/bull": ^3.15.9 + "@types/chai-as-promised": ^7.1.8 "@types/compression": ^1.7.2 "@types/cookie-parser": ^1.4.7 "@types/debug": ^4.1.7 "@types/deep-equal-in-any-order": ^1.0.1 "@types/ejs": ^3.1.1 "@types/express": ^4.17.13 + "@types/ioredis-mock": ^8.2.5 "@types/libsodium-wrappers": ^0 "@types/lodash": ^4.14.180 "@types/mailchimp__mailchimp_marketing": ^3.0.9 @@ -14325,12 +14037,14 @@ __metadata: "@types/zxcvbn": ^4.4.1 "@typescript-eslint/eslint-plugin": ^5.39.0 "@typescript-eslint/parser": ^5.39.0 + ajv: ^8.12.0 apollo-server-express: ^3.10.2 axios: ^1.6.0 bcrypt: ^5.0.0 bull: ^4.8.5 busboy: ^1.4.0 chai: ^4.2.0 + chai-as-promised: ^7.1.2 chai-http: ^4.3.0 compression: ^1.7.4 concurrently: ^7.0.0 @@ -14358,6 +14072,7 @@ __metadata: graphql-subscriptions: ^2.0.0 http-proxy-middleware: v3.0.0-beta.0 ioredis: ^5.2.2 + ioredis-mock: ^8.9.0 knex: ^2.4.1 libsodium-wrappers: ^0.7.13 lodash: ^4.17.21 @@ -14375,7 +14090,6 @@ __metadata: nodemailer: ^6.5.0 nodemon: ^2.0.20 nyc: ^15.0.1 - octokit: ^3.1.2 openid-client: ^5.1.7 passport: ^0.6.0 passport-azure-ad: ^4.3.4 @@ -14447,6 +14161,8 @@ __metadata: rollup-plugin-typescript2: ^0.34.1 type-fest: ^3.11.1 typescript: ^4.5.4 + znv: ^0.4.0 + zod: ^3.22.4 peerDependencies: "@tiptap/core": ^2.0.0-beta.176 pino: ^8.7.0 @@ -14606,6 +14322,7 @@ __metadata: "@speckle/shared": "workspace:^" "@types/babel__core": ^7.20.1 "@types/flat": ^5.0.2 + "@types/lodash-es": 4.17.12 "@types/three": ^0.136.0 "@typescript-eslint/eslint-plugin": ^5.39.0 "@typescript-eslint/parser": ^5.39.0 @@ -14614,13 +14331,11 @@ __metadata: core-js: ^3.21.1 eslint: ^8.11.0 eslint-config-prettier: ^8.5.0 - flat: ^5.0.2 hold-event: ^0.1.0 js-logger: 1.6.1 jsdom: ^24.0.0 lodash-es: ^4.17.21 prettier: ^2.5.1 - rainbowvis.js: ^1.0.1 regenerator-runtime: ^0.13.7 rollup: ^2.70.1 rollup-plugin-delete: ^2.0.0 @@ -14633,7 +14348,6 @@ __metadata: troika-three-text: 0.47.2 type-fest: ^4.15.0 typescript: ^4.5.4 - underscore: 1.13.6 vitest: ^1.4.0 languageName: unknown linkType: soft @@ -16445,13 +16159,6 @@ __metadata: languageName: node linkType: hard -"@types/aws-lambda@npm:^8.10.83": - version: 8.10.137 - resolution: "@types/aws-lambda@npm:8.10.137" - checksum: 172238b8a5d1e4002d11517f4e6739836806b59844da336ce44e72cd544c97453071ffdf6bedd736858e96569123988dd451055bf41ea3876e7201255d5c7713 - languageName: node - linkType: hard - "@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14": version: 7.1.19 resolution: "@types/babel__core@npm:7.1.19" @@ -16534,13 +16241,6 @@ __metadata: languageName: node linkType: hard -"@types/btoa-lite@npm:^1.0.0": - version: 1.0.2 - resolution: "@types/btoa-lite@npm:1.0.2" - checksum: 4c46b163c881a75522c7556dd7a7df8a0d4c680a45e8bac34e50864e1c2d9df8dc90b99f75199154c60ef2faff90896b7e5f11df6936c94167a3e5e1c6f4d935 - languageName: node - linkType: hard - "@types/bull@npm:^3.15.9": version: 3.15.9 resolution: "@types/bull@npm:3.15.9" @@ -16563,6 +16263,22 @@ __metadata: languageName: node linkType: hard +"@types/chai-as-promised@npm:^7.1.8": + version: 7.1.8 + resolution: "@types/chai-as-promised@npm:7.1.8" + dependencies: + "@types/chai": "*" + checksum: f0e5eab451b91bc1e289ed89519faf6591932e8a28d2ec9bbe95826eb73d28fe43713633e0c18706f3baa560a7d97e7c7c20dc53ce639e5d75bac46b2a50bf21 + languageName: node + linkType: hard + +"@types/chai@npm:*": + version: 4.3.16 + resolution: "@types/chai@npm:4.3.16" + checksum: bb5f52d1b70534ed8b4bf74bd248add003ffe1156303802ea367331607c06b494da885ffbc2b674a66b4f90c9ee88759790a5f243879f6759f124f22328f5e95 + languageName: node + linkType: hard + "@types/chai@npm:4": version: 4.3.1 resolution: "@types/chai@npm:4.3.1" @@ -16917,6 +16633,16 @@ __metadata: languageName: node linkType: hard +"@types/ioredis-mock@npm:^8.2.5": + version: 8.2.5 + resolution: "@types/ioredis-mock@npm:8.2.5" + dependencies: + "@types/node": "*" + ioredis: ">=5" + checksum: c32a20e02f8c777780b8663ba83bc7be4a5098fe44c390dd28cb69c9b60fc6dcfa1cbc7e5f8dca3579e85b3be1a08b5b8201a5a2d6d1195e5e5840b13538caab + languageName: node + linkType: hard + "@types/ioredis@npm:*": version: 4.28.10 resolution: "@types/ioredis@npm:4.28.10" @@ -17045,6 +16771,15 @@ __metadata: languageName: node linkType: hard +"@types/lodash-es@npm:4.17.12": + version: 4.17.12 + resolution: "@types/lodash-es@npm:4.17.12" + dependencies: + "@types/lodash": "*" + checksum: 990a99e2243bebe9505cb5ad19fbc172beb4a8e00f9075c99fc06c46c2801ffdb40bc2867271cf580d5f48994fc9fb076ec92cd60a20e621603bf22114e5b077 + languageName: node + linkType: hard + "@types/lodash-es@npm:^4.17.6": version: 4.17.6 resolution: "@types/lodash-es@npm:4.17.6" @@ -20297,7 +20032,7 @@ __metadata: languageName: node linkType: hard -"aggregate-error@npm:^3.0.0, aggregate-error@npm:^3.1.0": +"aggregate-error@npm:^3.0.0": version: 3.1.0 resolution: "aggregate-error@npm:3.1.0" dependencies: @@ -20365,6 +20100,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^8.12.0": + version: 8.13.0 + resolution: "ajv@npm:8.13.0" + dependencies: + fast-deep-equal: ^3.1.3 + json-schema-traverse: ^1.0.0 + require-from-string: ^2.0.2 + uri-js: ^4.4.1 + checksum: 6de82d0b2073e645ca3300561356ddda0234f39b35d2125a8700b650509b296f41c00ab69f53178bbe25ad688bd6ac3747ab44101f2f4bd245952e8fd6ccc3c1 + languageName: node + linkType: hard + "ajv@npm:^8.6.1": version: 8.12.0 resolution: "ajv@npm:8.12.0" @@ -21637,13 +21384,6 @@ __metadata: languageName: node linkType: hard -"before-after-hook@npm:^2.2.0": - version: 2.2.3 - resolution: "before-after-hook@npm:2.2.3" - checksum: a1a2430976d9bdab4cd89cb50d27fa86b19e2b41812bf1315923b0cba03371ebca99449809226425dd3bcef20e010db61abdaff549278e111d6480034bebae87 - languageName: node - linkType: hard - "better-opn@npm:^3.0.2": version: 3.0.2 resolution: "better-opn@npm:3.0.2" @@ -21749,6 +21489,13 @@ __metadata: languageName: node linkType: hard +"blakejs@npm:^1.1.0": + version: 1.2.1 + resolution: "blakejs@npm:1.2.1" + checksum: d699ba116cfa21d0b01d12014a03e484dd76d483133e6dc9eb415aa70a119f08beb3bcefb8c71840106a00b542cba77383f8be60cd1f0d4589cb8afb922eefbe + languageName: node + linkType: hard + "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": version: 4.12.0 resolution: "bn.js@npm:4.12.0" @@ -21822,13 +21569,6 @@ __metadata: languageName: node linkType: hard -"bottleneck@npm:^2.15.3": - version: 2.19.5 - resolution: "bottleneck@npm:2.19.5" - checksum: c5eef1bbea12cef1f1405e7306e7d24860568b0f7ac5eeab706a86762b3fc65ef6d1c641c8a166e4db90f412fc5c948fc5ce8008a8cd3d28c7212ef9c3482bda - languageName: node - linkType: hard - "bowser@npm:^2.11.0": version: 2.11.0 resolution: "bowser@npm:2.11.0" @@ -22074,13 +21814,6 @@ __metadata: languageName: node linkType: hard -"btoa-lite@npm:^1.0.0": - version: 1.0.0 - resolution: "btoa-lite@npm:1.0.0" - checksum: c2d61993b801f8e35a96f20692a45459c753d9baa29d86d1343e714f8d6bbe7069f1a20a5ae868488f3fb137d5bd0c560f6fbbc90b5a71050919d2d2c97c0475 - languageName: node - linkType: hard - "buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13, buffer-crc32@npm:~0.2.3": version: 0.2.13 resolution: "buffer-crc32@npm:0.2.13" @@ -22640,6 +22373,17 @@ __metadata: languageName: node linkType: hard +"chai-as-promised@npm:^7.1.2": + version: 7.1.2 + resolution: "chai-as-promised@npm:7.1.2" + dependencies: + check-error: ^1.0.2 + peerDependencies: + chai: ">= 2.1.2 < 6" + checksum: 671ee980054eb23a523875c1d22929a2ac05d89b5428e1fd12800f54fc69baf41014667b87e2368e2355ee2a3140d3e3d7d5a1f8638b07cfefd7fe38a149e3f6 + languageName: node + linkType: hard + "chai-http@npm:^4.3.0": version: 4.3.0 resolution: "chai-http@npm:4.3.0" @@ -25329,13 +25073,6 @@ __metadata: languageName: node linkType: hard -"deprecation@npm:^2.0.0, deprecation@npm:^2.3.1": - version: 2.3.1 - resolution: "deprecation@npm:2.3.1" - checksum: f56a05e182c2c195071385455956b0c4106fe14e36245b00c689ceef8e8ab639235176a96977ba7c74afb173317fac2e0ec6ec7a1c6d1e6eaa401c586c714132 - languageName: node - linkType: hard - "dequal@npm:^2.0.2": version: 2.0.3 resolution: "dequal@npm:2.0.3" @@ -27733,6 +27470,26 @@ __metadata: languageName: node linkType: hard +"fengari-interop@npm:^0.1.3": + version: 0.1.3 + resolution: "fengari-interop@npm:0.1.3" + peerDependencies: + fengari: ^0.1.0 + checksum: f483e0aedec3a0b49911ffd3207a55f73c861f95ef84fb7582deb45bc65afa2e7bf8f9fc3734563f6c106a438909f94059b5d08f5a7872ffcc3d45d260a3ee15 + languageName: node + linkType: hard + +"fengari@npm:^0.1.4": + version: 0.1.4 + resolution: "fengari@npm:0.1.4" + dependencies: + readline-sync: ^1.4.9 + sprintf-js: ^1.1.1 + tmp: ^0.0.33 + checksum: bd6b04f9738f9cbb58e3c80684a72bf6f7e74c902d26e4e812b1cfbd69c06da5ffb2a8a67a62ecd75603fc565b6b183c1b72be60f2e60c18aa487aad65b62196 + languageName: node + linkType: hard + "fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": version: 3.2.0 resolution: "fetch-blob@npm:3.2.0" @@ -30401,6 +30158,39 @@ __metadata: languageName: node linkType: hard +"ioredis-mock@npm:^8.9.0": + version: 8.9.0 + resolution: "ioredis-mock@npm:8.9.0" + dependencies: + "@ioredis/as-callback": ^3.0.0 + "@ioredis/commands": ^1.2.0 + fengari: ^0.1.4 + fengari-interop: ^0.1.3 + semver: ^7.5.4 + peerDependencies: + "@types/ioredis-mock": ^8 + ioredis: ^5 + checksum: 9d7480f153a5904f2bbb1555ebf2246f80bfd8c89e10442b6db58984809a1a652050c61fc8ad8a1fa86629ee0f6d2bac2929fd66f34e143263d0449bcdf657aa + languageName: node + linkType: hard + +"ioredis@npm:>=5": + version: 5.4.1 + resolution: "ioredis@npm:5.4.1" + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.1.0 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: 92210294f75800febe7544c27b07e4892480172363b11971aa575be5b68f023bfed4bc858abc9792230c153aa80409047a358f174062c14d17536aa4499fe10b + languageName: node + linkType: hard + "ioredis@npm:^4.17.3, ioredis@npm:^4.28.5": version: 4.28.5 resolution: "ioredis@npm:4.28.5" @@ -33070,24 +32860,6 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:^9.0.2": - version: 9.0.2 - resolution: "jsonwebtoken@npm:9.0.2" - dependencies: - jws: ^3.2.2 - lodash.includes: ^4.3.0 - lodash.isboolean: ^3.0.3 - lodash.isinteger: ^4.0.4 - lodash.isnumber: ^3.0.3 - lodash.isplainobject: ^4.0.6 - lodash.isstring: ^4.0.1 - lodash.once: ^4.0.0 - ms: ^2.1.1 - semver: ^7.5.4 - checksum: fc739a6a8b33f1974f9772dca7f8493ca8df4cc31c5a09dcfdb7cff77447dcf22f4236fb2774ef3fe50df0abeb8e1c6f4c41eba82f500a804ab101e2fbc9d61a - languageName: node - linkType: hard - "jsprim@npm:^1.2.2": version: 1.4.2 resolution: "jsprim@npm:1.4.2" @@ -33868,13 +33640,6 @@ __metadata: languageName: node linkType: hard -"lodash.includes@npm:^4.3.0": - version: 4.3.0 - resolution: "lodash.includes@npm:4.3.0" - checksum: 71092c130515a67ab3bd928f57f6018434797c94def7f46aafa417771e455ce3a4834889f4267b17887d7f75297dfabd96231bf704fd2b8c5096dc4a913568b6 - languageName: node - linkType: hard - "lodash.isarguments@npm:^3.1.0": version: 3.1.0 resolution: "lodash.isarguments@npm:3.1.0" @@ -33882,13 +33647,6 @@ __metadata: languageName: node linkType: hard -"lodash.isboolean@npm:^3.0.3": - version: 3.0.3 - resolution: "lodash.isboolean@npm:3.0.3" - checksum: b70068b4a8b8837912b54052557b21fc4774174e3512ed3c5b94621e5aff5eb6c68089d0a386b7e801d679cd105d2e35417978a5e99071750aa2ed90bffd0250 - languageName: node - linkType: hard - "lodash.isempty@npm:^4.4.0": version: 4.4.0 resolution: "lodash.isempty@npm:4.4.0" @@ -33910,20 +33668,6 @@ __metadata: languageName: node linkType: hard -"lodash.isinteger@npm:^4.0.4": - version: 4.0.4 - resolution: "lodash.isinteger@npm:4.0.4" - checksum: 6034821b3fc61a2ffc34e7d5644bb50c5fd8f1c0121c554c21ac271911ee0c0502274852845005f8651d51e199ee2e0cfebfe40aaa49c7fe617f603a8a0b1691 - languageName: node - linkType: hard - -"lodash.isnumber@npm:^3.0.3": - version: 3.0.3 - resolution: "lodash.isnumber@npm:3.0.3" - checksum: 913784275b565346255e6ae6a6e30b760a0da70abc29f3e1f409081585875105138cda4a429ff02577e1bc0a7ae2a90e0a3079a37f3a04c3d6c5aaa532f4cab2 - languageName: node - linkType: hard - "lodash.isplainobject@npm:^4.0.6": version: 4.0.6 resolution: "lodash.isplainobject@npm:4.0.6" @@ -33931,13 +33675,6 @@ __metadata: languageName: node linkType: hard -"lodash.isstring@npm:^4.0.1": - version: 4.0.1 - resolution: "lodash.isstring@npm:4.0.1" - checksum: eaac87ae9636848af08021083d796e2eea3d02e80082ab8a9955309569cb3a463ce97fd281d7dc119e402b2e7d8c54a23914b15d2fc7fff56461511dc8937ba0 - languageName: node - linkType: hard - "lodash.isundefined@npm:^3.0.1": version: 3.0.1 resolution: "lodash.isundefined@npm:3.0.1" @@ -33980,13 +33717,6 @@ __metadata: languageName: node linkType: hard -"lodash.once@npm:^4.0.0": - version: 4.1.1 - resolution: "lodash.once@npm:4.1.1" - checksum: d768fa9f9b4e1dc6453be99b753906f58990e0c45e7b2ca5a3b40a33111e5d17f6edf2f768786e2716af90a8e78f8f91431ab8435f761fef00f9b0c256f6d245 - languageName: node - linkType: hard - "lodash.padend@npm:^4.6.1": version: 4.6.1 resolution: "lodash.padend@npm:4.6.1" @@ -37718,24 +37448,6 @@ __metadata: languageName: node linkType: hard -"octokit@npm:^3.1.2": - version: 3.2.0 - resolution: "octokit@npm:3.2.0" - dependencies: - "@octokit/app": ^14.0.2 - "@octokit/core": ^5.0.0 - "@octokit/oauth-app": ^6.0.0 - "@octokit/plugin-paginate-graphql": ^4.0.0 - "@octokit/plugin-paginate-rest": ^9.0.0 - "@octokit/plugin-rest-endpoint-methods": ^10.0.0 - "@octokit/plugin-retry": ^6.0.0 - "@octokit/plugin-throttling": ^8.0.0 - "@octokit/request-error": ^5.0.0 - "@octokit/types": ^12.0.0 - checksum: 3ec8efe02144aa6210a5c846947245cbe58d31144fa8e6a1726782001d77fcd044b7e1498a56c5e47aba44c1f5726a37c261f474cb46b816579549f7a09db1f6 - languageName: node - linkType: hard - "ofetch@npm:^1.1.1": version: 1.1.1 resolution: "ofetch@npm:1.1.1" @@ -41598,6 +41310,13 @@ __metadata: languageName: node linkType: hard +"readline-sync@npm:^1.4.9": + version: 1.4.10 + resolution: "readline-sync@npm:1.4.10" + checksum: 4dbd8925af028dc4cb1bb813f51ca3479035199aa5224886b560eec8e768ab27d7ebf11d69a67ed93d5a130b7c994f0bdb77796326e563cf928bbfd560e3747e + languageName: node + linkType: hard + "real-require@npm:^0.2.0": version: 0.2.0 resolution: "real-require@npm:0.2.0" @@ -42664,6 +42383,7 @@ __metadata: "@types/eslint": ^8.4.1 "@types/lockfile": ^1.0.2 commitizen: ^4.2.5 + cross-env: ^7.0.3 cz-conventional-changelog: ^3.3.0 eslint: ^8.11.0 eslint-config-prettier: ^8.5.0 @@ -43764,7 +43484,7 @@ __metadata: languageName: node linkType: hard -"sprintf-js@npm:^1.1.3": +"sprintf-js@npm:^1.1.1, sprintf-js@npm:^1.1.3": version: 1.1.3 resolution: "sprintf-js@npm:1.1.3" checksum: a3fdac7b49643875b70864a9d9b469d87a40dfeaf5d34d9d0c5b1cda5fd7d065531fcb43c76357d62254c57184a7b151954156563a4d6a747015cfb41021cad0 @@ -45697,6 +45417,23 @@ __metadata: languageName: node linkType: hard +"tweetnacl-sealedbox-js@npm:^1.2.0": + version: 1.2.0 + resolution: "tweetnacl-sealedbox-js@npm:1.2.0" + dependencies: + blakejs: ^1.1.0 + tweetnacl: ^1.0.1 + checksum: ada2e3827bd0b49a23d576813fb76aa0815ccafa07c8c3dc1a86c56bc56f4f0ec605f255a2f7e4ce0be83f94906bbbb151eeb2edeb12e95420c60c0b34d89d50 + languageName: node + linkType: hard + +"tweetnacl-util@npm:^0.15.1": + version: 0.15.1 + resolution: "tweetnacl-util@npm:0.15.1" + checksum: ae6aa8a52cdd21a95103a4cc10657d6a2040b36c7a6da7b9d3ab811c6750a2d5db77e8c36969e75fdee11f511aa2b91c552496c6e8e989b6e490e54aca2864fc + languageName: node + linkType: hard + "tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0": version: 0.14.5 resolution: "tweetnacl@npm:0.14.5" @@ -45704,6 +45441,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl@npm:^1.0.1": + version: 1.0.3 + resolution: "tweetnacl@npm:1.0.3" + checksum: e4a57cac188f0c53f24c7a33279e223618a2bfb5fea426231991652a13247bea06b081fd745d71291fcae0f4428d29beba1b984b1f1ce6f66b06a6d1ab90645c + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -46370,23 +46114,6 @@ __metadata: languageName: node linkType: hard -"universal-github-app-jwt@npm:^1.1.2": - version: 1.1.2 - resolution: "universal-github-app-jwt@npm:1.1.2" - dependencies: - "@types/jsonwebtoken": ^9.0.0 - jsonwebtoken: ^9.0.2 - checksum: 1bc069c57d319607d4b52143ba89de18cdff2b6afb63107e6972dff9574c7fc453f1a6bb1714817c72898a55c37fa38783be965ebd1c61de661231ca061440d1 - languageName: node - linkType: hard - -"universal-user-agent@npm:^6.0.0": - version: 6.0.1 - resolution: "universal-user-agent@npm:6.0.1" - checksum: fdc8e1ae48a05decfc7ded09b62071f571c7fe0bd793d700704c80cea316101d4eac15cc27ed2bb64f4ce166d2684777c3198b9ab16034f547abea0d3aa1c93c - languageName: node - linkType: hard - "universalify@npm:^0.1.0": version: 0.1.2 resolution: "universalify@npm:0.1.2" @@ -46938,7 +46665,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2": +"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: