Merge branch 'main' into oguzhan/acc-connector
This commit is contained in:
@@ -5,44 +5,40 @@ description = "Speckle IFC importer worker app"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"asyncpg>=0.30.0",
|
||||
"typed-settings>=24.5.0",
|
||||
"pydantic>=2.11.7",
|
||||
"python-dotenv>=1.0.0",
|
||||
"structlog>=25.4.0",
|
||||
"structlog-to-seq>=21.0.0",
|
||||
"specklepy[speckleifc]>=3.0.4.dev8",
|
||||
"asyncpg>=0.30.0",
|
||||
"typed-settings>=24.5.0",
|
||||
"pydantic>=2.11.7",
|
||||
"python-dotenv>=1.0.0",
|
||||
"structlog>=25.4.0",
|
||||
"structlog-to-seq>=21.0.0",
|
||||
"specklepy[speckleifc]>=3.0.4.dev8",
|
||||
"colorful>=0.5.7",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"asyncpg-stubs>=0.30.2",
|
||||
"colorama>=0.4.6",
|
||||
"colorful>=0.5.7",
|
||||
"ruff>=0.12.2",
|
||||
]
|
||||
dev = ["asyncpg-stubs>=0.30.2", "colorama>=0.4.6", "ruff>=0.12.2"]
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [".venv", "**/*.yml"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"A",
|
||||
# pycodestyle
|
||||
"E",
|
||||
# Pyflakes
|
||||
"F",
|
||||
# pyupgrade
|
||||
"UP",
|
||||
# flake8-bugbear
|
||||
"B",
|
||||
# flake8-simplify
|
||||
"SIM",
|
||||
# isort
|
||||
"I",
|
||||
# PEP8 naming
|
||||
"N",
|
||||
"ASYNC",
|
||||
"A",
|
||||
# pycodestyle
|
||||
"E",
|
||||
# Pyflakes
|
||||
"F",
|
||||
# pyupgrade
|
||||
"UP",
|
||||
# flake8-bugbear
|
||||
"B",
|
||||
# flake8-simplify
|
||||
"SIM",
|
||||
# isort
|
||||
"I",
|
||||
# PEP8 naming
|
||||
"N",
|
||||
"ASYNC",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -31,6 +31,7 @@ class JobStatus(StrEnum):
|
||||
"""Status enumeration for the job."""
|
||||
|
||||
QUEUED = "queued"
|
||||
PROCESSING = "processing"
|
||||
SUCCEEDED = "succeeded"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@@ -71,96 +71,100 @@ async def job_processor(logger: structlog.stdlib.BoundLogger):
|
||||
parser = "speckle_ifc"
|
||||
connection = await setup_connection()
|
||||
while True:
|
||||
async with connection.transaction():
|
||||
job = await get_next_job(connection)
|
||||
if not job:
|
||||
logger.info("no job found")
|
||||
await asyncio.sleep(IDLE_TIMEOUT)
|
||||
continue
|
||||
job = await get_next_job(connection)
|
||||
if not job:
|
||||
logger.info("no job found")
|
||||
await asyncio.sleep(IDLE_TIMEOUT)
|
||||
continue
|
||||
|
||||
start = time.time()
|
||||
speckle_client = setup_client(job.payload)
|
||||
start = time.time()
|
||||
speckle_client = setup_client(job.payload)
|
||||
|
||||
job_id = job.id
|
||||
job_status = JobStatus.QUEUED
|
||||
ex: Exception | None = None
|
||||
attempt = job.attempt + 1
|
||||
job_id = job.id
|
||||
job_status = JobStatus.QUEUED
|
||||
ex: Exception | None = None
|
||||
attempt = job.attempt
|
||||
version_id: str | None = None
|
||||
|
||||
version_id: str | None = None
|
||||
|
||||
try:
|
||||
logger = logger.bind(job_id=job_id, project_id=job.payload.project_id)
|
||||
logger.info("starting job")
|
||||
handler = job_handler(speckle_client, job.payload, logger)
|
||||
# this will raise a TimeoutError if handler does not complete in time
|
||||
version, download_duration, parse_duration = await asyncio.wait_for(
|
||||
handler, timeout=job.payload.time_out_seconds
|
||||
)
|
||||
version_id = version.id
|
||||
|
||||
duration = time.time() - start
|
||||
logger.info(
|
||||
"Finished job after {duration} created version {version_id}",
|
||||
duration=duration,
|
||||
version_id=version_id,
|
||||
try:
|
||||
if attempt > job.max_attempt:
|
||||
# something went wrong, it should have been marked as failed
|
||||
raise Exception(
|
||||
"Unhandled error silently failed the job multiple times"
|
||||
)
|
||||
|
||||
_ = speckle_client.file_import.finish_file_import_job(
|
||||
FileImportSuccessInput(
|
||||
project_id=job.payload.project_id,
|
||||
# for some reason, the blob id identifies the job here
|
||||
job_id=job.payload.blob_id,
|
||||
result=FileImportResult(
|
||||
parser=parser,
|
||||
version_id=version_id,
|
||||
download_duration_seconds=download_duration,
|
||||
duration_seconds=duration,
|
||||
parse_duration_seconds=parse_duration,
|
||||
),
|
||||
)
|
||||
logger = logger.bind(job_id=job_id, project_id=job.payload.project_id)
|
||||
logger.info("starting job")
|
||||
handler = job_handler(speckle_client, job.payload, logger)
|
||||
# this will raise a TimeoutError if handler does not complete in time
|
||||
version, download_duration, parse_duration = await asyncio.wait_for(
|
||||
handler, timeout=job.payload.time_out_seconds
|
||||
)
|
||||
version_id = version.id
|
||||
|
||||
duration = time.time() - start
|
||||
logger.info(
|
||||
"Finished job after {duration} created version {version_id}",
|
||||
duration=duration,
|
||||
version_id=version_id,
|
||||
)
|
||||
|
||||
_ = speckle_client.file_import.finish_file_import_job(
|
||||
FileImportSuccessInput(
|
||||
project_id=job.payload.project_id,
|
||||
# for some reason, the blob id identifies the job here
|
||||
job_id=job.payload.blob_id,
|
||||
result=FileImportResult(
|
||||
parser=parser,
|
||||
version_id=version_id,
|
||||
download_duration_seconds=download_duration,
|
||||
duration_seconds=duration,
|
||||
parse_duration_seconds=parse_duration,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
job_status = JobStatus.SUCCEEDED
|
||||
job_status = JobStatus.SUCCEEDED
|
||||
|
||||
except TimeoutError as te:
|
||||
# if it times out we allow re-queueing until it reaches max tries
|
||||
ex = te
|
||||
if attempt >= job.max_attempt:
|
||||
job_status = JobStatus.FAILED
|
||||
else:
|
||||
job_status = JobStatus.QUEUED
|
||||
|
||||
# raised if the task is canceled
|
||||
except Exception as e:
|
||||
#
|
||||
ex = e
|
||||
except TimeoutError as te:
|
||||
# if it times out we allow re-queueing until it reaches max tries
|
||||
ex = te
|
||||
if attempt >= job.max_attempt:
|
||||
job_status = JobStatus.FAILED
|
||||
finally:
|
||||
if job_status == JobStatus.FAILED:
|
||||
# we should be reporting the failure to the server
|
||||
logger.error("job processing failed", exc_info=ex)
|
||||
try:
|
||||
_ = speckle_client.file_import.finish_file_import_job(
|
||||
FileImportErrorInput(
|
||||
project_id=job.payload.project_id,
|
||||
# for some reason, the blob id identifies the job here
|
||||
job_id=job.payload.blob_id,
|
||||
# job_id=job_id,
|
||||
reason=str(ex),
|
||||
result=FileImportResult(
|
||||
parser=parser,
|
||||
version_id=None,
|
||||
download_duration_seconds=0,
|
||||
duration_seconds=time.time() - start,
|
||||
parse_duration_seconds=0,
|
||||
),
|
||||
)
|
||||
else:
|
||||
job_status = JobStatus.QUEUED
|
||||
|
||||
# raised if the task is canceled
|
||||
except Exception as e:
|
||||
#
|
||||
ex = e
|
||||
job_status = JobStatus.FAILED
|
||||
finally:
|
||||
if job_status == JobStatus.FAILED:
|
||||
# we should be reporting the failure to the server
|
||||
logger.error("job processing failed", exc_info=ex)
|
||||
try:
|
||||
_ = speckle_client.file_import.finish_file_import_job(
|
||||
FileImportErrorInput(
|
||||
project_id=job.payload.project_id,
|
||||
# for some reason, the blob id identifies the job here
|
||||
job_id=job.payload.blob_id,
|
||||
# job_id=job_id,
|
||||
reason=str(ex),
|
||||
result=FileImportResult(
|
||||
parser=parser,
|
||||
version_id=None,
|
||||
download_duration_seconds=0,
|
||||
duration_seconds=time.time() - start,
|
||||
parse_duration_seconds=0,
|
||||
),
|
||||
)
|
||||
# if the reporting of the failure does not succeed, we're requeueing
|
||||
# unless we've reached the max attempts
|
||||
except Exception as ex:
|
||||
if attempt >= job.max_attempt:
|
||||
job_status = JobStatus.FAILED
|
||||
else:
|
||||
job_status = JobStatus.QUEUED
|
||||
await set_job_status(connection, job_id, job_status, attempt)
|
||||
)
|
||||
# if the reporting of the failure does not succeed, we're requeueing
|
||||
# unless we've reached the max attempts
|
||||
except Exception as ex:
|
||||
if attempt >= job.max_attempt:
|
||||
job_status = JobStatus.FAILED
|
||||
else:
|
||||
job_status = JobStatus.QUEUED
|
||||
await set_job_status(connection, job_id, job_status)
|
||||
|
||||
@@ -22,12 +22,32 @@ async def get_next_job(connection: Connection) -> FileimportJob | None:
|
||||
|
||||
job = await connection.fetchrow(
|
||||
"""
|
||||
SELECT * FROM background_jobs
|
||||
WHERE payload ->> 'fileType' = 'ifc' AND status = $1 AND attempt < "maxAttempt"
|
||||
ORDER BY "createdAt"
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 1
|
||||
WITH next_job AS (
|
||||
UPDATE background_jobs
|
||||
SET
|
||||
"attempt" = "attempt" + 1,
|
||||
"status" = $1,
|
||||
"updatedAt" = NOW()
|
||||
WHERE id = (
|
||||
SELECT id FROM background_jobs
|
||||
WHERE ( --queued job
|
||||
payload ->> 'fileType' = 'ifc'
|
||||
AND status = $2
|
||||
)
|
||||
OR ( --timed job left on processing state
|
||||
payload ->> 'fileType' = 'ifc'
|
||||
AND status = $1
|
||||
AND "updatedAt" < NOW() - ("timeoutMs" * interval '1 millisecond')
|
||||
)
|
||||
ORDER BY "createdAt"
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 1
|
||||
)
|
||||
RETURNING *
|
||||
)
|
||||
SELECT * FROM next_job;
|
||||
""",
|
||||
JobStatus.PROCESSING.value,
|
||||
JobStatus.QUEUED.value,
|
||||
)
|
||||
if not job:
|
||||
@@ -36,16 +56,15 @@ async def get_next_job(connection: Connection) -> FileimportJob | None:
|
||||
|
||||
|
||||
async def set_job_status(
|
||||
connection: Connection, job_id: str, job_status: JobStatus, attempt: int
|
||||
connection: Connection, job_id: str, job_status: JobStatus
|
||||
) -> None:
|
||||
print(f"updating job: {job_id}'s status to {job_status}, with attempt: {attempt}")
|
||||
print(f"updating job: {job_id}'s status to {job_status}")
|
||||
_ = await connection.execute(
|
||||
"""
|
||||
UPDATE background_jobs
|
||||
SET status = $1, "updatedAt" = NOW(), attempt = $3
|
||||
SET status = $1, "updatedAt" = NOW()
|
||||
WHERE id = $2
|
||||
""",
|
||||
job_status.value,
|
||||
job_id,
|
||||
attempt,
|
||||
)
|
||||
|
||||
Generated
+2
-2
@@ -251,6 +251,7 @@ version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "asyncpg" },
|
||||
{ name = "colorful" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "specklepy", extra = ["speckleifc"] },
|
||||
@@ -263,13 +264,13 @@ dependencies = [
|
||||
dev = [
|
||||
{ name = "asyncpg-stubs" },
|
||||
{ name = "colorama" },
|
||||
{ name = "colorful" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "asyncpg", specifier = ">=0.30.0" },
|
||||
{ name = "colorful", specifier = ">=0.5.7" },
|
||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
||||
{ name = "specklepy", extras = ["speckleifc"], specifier = ">=3.0.4.dev8" },
|
||||
@@ -282,7 +283,6 @@ requires-dist = [
|
||||
dev = [
|
||||
{ name = "asyncpg-stubs", specifier = ">=0.30.2" },
|
||||
{ name = "colorama", specifier = ">=0.4.6" },
|
||||
{ name = "colorful", specifier = ">=0.5.7" },
|
||||
{ name = "ruff", specifier = ">=0.12.2" },
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import prometheusClient from 'prom-client'
|
||||
import { join } from 'lodash-es'
|
||||
import type { MetricInitializer } from '@/observability/types.js'
|
||||
|
||||
export const init: MetricInitializer = (config) => {
|
||||
const { labelNames, namePrefix, logger } = config
|
||||
const metric = new prometheusClient.Gauge({
|
||||
name: join([namePrefix, 'db', 'max_prepared_transactions'], '_'),
|
||||
help: 'Configured value of max_prepared_transactions for the Postgres database',
|
||||
labelNames: ['region', ...labelNames]
|
||||
})
|
||||
return async (params) => {
|
||||
const { dbClients, labels } = params
|
||||
await Promise.all(
|
||||
dbClients.map(async ({ client, regionKey }) => {
|
||||
const queryResults = await client.raw<{
|
||||
rows: [{ max_prepared_transactions: string }]
|
||||
}>(`SHOW max_prepared_transactions;`)
|
||||
if (!queryResults.rows.length) {
|
||||
logger.error(
|
||||
{ region: regionKey },
|
||||
"No max_prepared_transactions found for region '{region}'. This is odd."
|
||||
)
|
||||
return
|
||||
}
|
||||
metric.set(
|
||||
{ ...labels, region: regionKey },
|
||||
parseInt(queryResults.rows[0].max_prepared_transactions)
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import prometheusClient from 'prom-client'
|
||||
import { join } from 'lodash-es'
|
||||
import type { MetricInitializer } from '@/observability/types.js'
|
||||
|
||||
export const init: MetricInitializer = (config) => {
|
||||
const { labelNames, namePrefix, logger } = config
|
||||
const dbWorkers = new prometheusClient.Gauge({
|
||||
name: join([namePrefix, 'db_prepared_transactions'], '_'),
|
||||
help: 'Number of prepared transactions',
|
||||
labelNames: ['region', ...labelNames]
|
||||
})
|
||||
return async (params) => {
|
||||
const { dbClients, labels } = params
|
||||
await Promise.all(
|
||||
dbClients.map(async ({ client, regionKey }) => {
|
||||
const connectionResults = await client.raw<{
|
||||
rows: [{ prepared_transaction_count: string }]
|
||||
}>(`SELECT COUNT(*) AS prepared_transaction_count FROM pg_prepared_xacts;`)
|
||||
if (!connectionResults.rows.length) {
|
||||
logger.error(
|
||||
{ region: regionKey },
|
||||
"No prepared transactions found for region '{region}'. This is odd."
|
||||
)
|
||||
return
|
||||
}
|
||||
dbWorkers.set(
|
||||
{ ...labels, region: regionKey },
|
||||
parseInt(connectionResults.rows[0].prepared_transaction_count)
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import { Counter, Histogram, Registry } from 'prom-client'
|
||||
import prometheusClient from 'prom-client'
|
||||
import { init as commits } from '@/observability/metrics/commits.js'
|
||||
import { init as dbMaxLogicalReplicationWorkers } from '@/observability/metrics/dbMaxLogicalReplicationWorkers.js'
|
||||
import { init as dbMaxPerparedTransactions } from '@/observability/metrics/dbMaxPerparedTransactions.js'
|
||||
import { init as dbMaxReplicationSlots } from '@/observability/metrics/dbMaxReplicationSlots.js'
|
||||
import { init as dbMaxSyncWorkersPerSubscription } from '@/observability/metrics/dbMaxSyncWorkersPerSubscription.js'
|
||||
import { init as dbMaxWalSenders } from '@/observability/metrics/dbMaxWalSenders.js'
|
||||
import { init as dbMaxWorkerProcesses } from '@/observability/metrics/dbMaxWorkerProcesses.js'
|
||||
import { init as dbPreparedTransactions } from '@/observability/metrics/dbPreparedTransactions.js'
|
||||
import { init as dbSize } from '@/observability/metrics/dbSize.js'
|
||||
import { init as dbWalLevel } from '@/observability/metrics/dbWalLevel.js'
|
||||
import { init as dbWorkers } from '@/observability/metrics/dbWorkers.js'
|
||||
@@ -62,10 +64,12 @@ function initMonitoringMetrics(params: {
|
||||
const metricsToInitialize = [
|
||||
commits,
|
||||
dbMaxLogicalReplicationWorkers,
|
||||
dbMaxPerparedTransactions,
|
||||
dbMaxReplicationSlots,
|
||||
dbMaxSyncWorkersPerSubscription,
|
||||
dbMaxWalSenders,
|
||||
dbMaxWorkerProcesses,
|
||||
dbPreparedTransactions,
|
||||
dbWalLevel,
|
||||
dbSize,
|
||||
dbWorkers,
|
||||
|
||||
@@ -4,7 +4,8 @@ import { moduleLogger } from '@/observability/logging'
|
||||
const backgroundJobsModule: SpeckleModule = {
|
||||
async init() {
|
||||
moduleLogger.info('🛠️ Init backgroundjobs module')
|
||||
}
|
||||
},
|
||||
async shutdown() {}
|
||||
}
|
||||
|
||||
export default backgroundJobsModule
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { Knex } from 'knex'
|
||||
import type {
|
||||
BackgroundJob,
|
||||
BackgroundJobPayload,
|
||||
GetBackgroundJob,
|
||||
GetBackgroundJobCount,
|
||||
StoreBackgroundJob
|
||||
import {
|
||||
type BackgroundJob,
|
||||
type BackgroundJobPayload,
|
||||
type GetBackgroundJob,
|
||||
type GetBackgroundJobCount,
|
||||
type StoreBackgroundJob
|
||||
} from '@/modules/backgroundjobs/domain'
|
||||
import { BackgroundJobStatus } from '@/modules/backgroundjobs/domain'
|
||||
import { buildTableHelper } from '@/modules/core/dbSchema'
|
||||
|
||||
export const BackgroundJobs = buildTableHelper('background_jobs', [
|
||||
@@ -54,22 +53,8 @@ export const getBackgroundJobCountFactory =
|
||||
async ({ status, jobType, minAttempts }) => {
|
||||
const q = tables.backgroundJobs(db).select(BackgroundJobs.col.id)
|
||||
|
||||
// using less restrictive lock to check locked jobs
|
||||
if (status === BackgroundJobStatus.Processing) {
|
||||
q.whereNotExists(function () {
|
||||
const subquery = this.from(BackgroundJobs.name)
|
||||
.select(BackgroundJobs.col.id)
|
||||
.whereRaw('id = background_jobs.id')
|
||||
.where({ jobType })
|
||||
|
||||
if (minAttempts) {
|
||||
q.andWhere(BackgroundJobs.col.attempt, '>=', minAttempts)
|
||||
}
|
||||
|
||||
subquery.forKeyShare().skipLocked()
|
||||
})
|
||||
} else {
|
||||
q.where({ status }).forKeyShare().skipLocked()
|
||||
if (status) {
|
||||
q.where({ status })
|
||||
}
|
||||
|
||||
if (minAttempts) {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import type {
|
||||
import { BackgroundJobStatus } from '@/modules/backgroundjobs/domain'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
export const scheduleBackgroundJobFactory = <T extends BackgroundJobPayload>({
|
||||
export const createBackgroundJobFactory = <T extends BackgroundJobPayload>({
|
||||
storeBackgroundJob,
|
||||
jobConfig
|
||||
}: {
|
||||
@@ -1,4 +1,4 @@
|
||||
import knex, { db } from '@/db/knex'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
storeBackgroundJobFactory,
|
||||
getBackgroundJobFactory,
|
||||
@@ -135,13 +135,10 @@ describe('Background Jobs repositories @backgroundjobs', () => {
|
||||
it('is able to count locked jobs', async () => {
|
||||
const job = createTestJob({
|
||||
jobType: 'fileImport',
|
||||
status: BackgroundJobStatus.Queued
|
||||
status: BackgroundJobStatus.Processing
|
||||
})
|
||||
await storeBackgroundJob({ job })
|
||||
|
||||
const trx = await knex.transaction()
|
||||
await trx().from(BackgroundJobs.name).where({ id: job.id }).forUpdate().first() // acquire lock
|
||||
|
||||
const [processingCount, queuedCount] = await Promise.all([
|
||||
getBackgroundJobCount({ status: 'processing', jobType: 'fileImport' }),
|
||||
getBackgroundJobCount({ status: 'queued', jobType: 'fileImport' })
|
||||
@@ -149,8 +146,6 @@ describe('Background Jobs repositories @backgroundjobs', () => {
|
||||
|
||||
expect(processingCount).to.equal(1)
|
||||
expect(queuedCount).to.equal(0)
|
||||
|
||||
await trx.commit()
|
||||
})
|
||||
|
||||
it('filters by min attempts', async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from 'chai'
|
||||
import { scheduleBackgroundJobFactory } from '@/modules/backgroundjobs/services'
|
||||
import { createBackgroundJobFactory } from '@/modules/backgroundjobs/services/create'
|
||||
import type {
|
||||
BackgroundJob,
|
||||
BackgroundJobConfig,
|
||||
@@ -41,12 +41,12 @@ describe('scheduleBackgroundJobFactory', () => {
|
||||
|
||||
describe('when called with valid parameters', () => {
|
||||
it('should create and store a background job with correct properties', async () => {
|
||||
const scheduleBackgroundJob = scheduleBackgroundJobFactory({
|
||||
const createBackgroundJob = createBackgroundJobFactory({
|
||||
storeBackgroundJob: mockStoreBackgroundJob,
|
||||
jobConfig: mockJobConfig
|
||||
})
|
||||
|
||||
const result = await scheduleBackgroundJob({ jobPayload: mockJobPayload })
|
||||
const result = await createBackgroundJob({ jobPayload: mockJobPayload })
|
||||
|
||||
expect(storeBackgroundJobCalled).to.be.true
|
||||
expect(storedJob).to.not.be.null
|
||||
@@ -54,58 +54,58 @@ describe('scheduleBackgroundJobFactory', () => {
|
||||
})
|
||||
|
||||
it('should generate a unique job ID', async () => {
|
||||
const scheduleBackgroundJob = scheduleBackgroundJobFactory({
|
||||
const createBackgroundJob = createBackgroundJobFactory({
|
||||
storeBackgroundJob: mockStoreBackgroundJob,
|
||||
jobConfig: mockJobConfig
|
||||
})
|
||||
|
||||
const result = await scheduleBackgroundJob({ jobPayload: mockJobPayload })
|
||||
const result = await createBackgroundJob({ jobPayload: mockJobPayload })
|
||||
|
||||
expect(result.id).to.be.a('string')
|
||||
expect(result.id).to.have.length(10)
|
||||
})
|
||||
|
||||
it('should set job status to Queued', async () => {
|
||||
const scheduleBackgroundJob = scheduleBackgroundJobFactory({
|
||||
const createBackgroundJob = createBackgroundJobFactory({
|
||||
storeBackgroundJob: mockStoreBackgroundJob,
|
||||
jobConfig: mockJobConfig
|
||||
})
|
||||
|
||||
const result = await scheduleBackgroundJob({ jobPayload: mockJobPayload })
|
||||
const result = await createBackgroundJob({ jobPayload: mockJobPayload })
|
||||
|
||||
expect(result.status).to.equal(BackgroundJobStatus.Queued)
|
||||
})
|
||||
|
||||
it('should set attempt to 0', async () => {
|
||||
const scheduleBackgroundJob = scheduleBackgroundJobFactory({
|
||||
const createBackgroundJob = createBackgroundJobFactory({
|
||||
storeBackgroundJob: mockStoreBackgroundJob,
|
||||
jobConfig: mockJobConfig
|
||||
})
|
||||
|
||||
const result = await scheduleBackgroundJob({ jobPayload: mockJobPayload })
|
||||
const result = await createBackgroundJob({ jobPayload: mockJobPayload })
|
||||
|
||||
expect(result.attempt).to.equal(0)
|
||||
})
|
||||
|
||||
it('should include job config properties', async () => {
|
||||
const scheduleBackgroundJob = scheduleBackgroundJobFactory({
|
||||
const createBackgroundJob = createBackgroundJobFactory({
|
||||
storeBackgroundJob: mockStoreBackgroundJob,
|
||||
jobConfig: mockJobConfig
|
||||
})
|
||||
|
||||
const result = await scheduleBackgroundJob({ jobPayload: mockJobPayload })
|
||||
const result = await createBackgroundJob({ jobPayload: mockJobPayload })
|
||||
|
||||
expect(result.maxAttempt).to.equal(mockJobConfig.maxAttempt)
|
||||
expect(result.timeoutMs).to.equal(mockJobConfig.timeoutMs)
|
||||
})
|
||||
|
||||
it('should preserve job payload', async () => {
|
||||
const scheduleBackgroundJob = scheduleBackgroundJobFactory({
|
||||
const createBackgroundJob = createBackgroundJobFactory({
|
||||
storeBackgroundJob: mockStoreBackgroundJob,
|
||||
jobConfig: mockJobConfig
|
||||
})
|
||||
|
||||
const result = await scheduleBackgroundJob({ jobPayload: mockJobPayload })
|
||||
const result = await createBackgroundJob({ jobPayload: mockJobPayload })
|
||||
|
||||
expect(result.payload).to.deep.equal(mockJobPayload)
|
||||
expect(result.jobType).to.equal(mockJobPayload.jobType)
|
||||
@@ -114,12 +114,12 @@ describe('scheduleBackgroundJobFactory', () => {
|
||||
it('should set createdAt and updatedAt timestamps', async () => {
|
||||
const beforeTest = new Date()
|
||||
|
||||
const scheduleBackgroundJob = scheduleBackgroundJobFactory({
|
||||
const createBackgroundJob = createBackgroundJobFactory({
|
||||
storeBackgroundJob: mockStoreBackgroundJob,
|
||||
jobConfig: mockJobConfig
|
||||
})
|
||||
|
||||
const result = await scheduleBackgroundJob({ jobPayload: mockJobPayload })
|
||||
const result = await createBackgroundJob({ jobPayload: mockJobPayload })
|
||||
|
||||
const afterTest = new Date()
|
||||
|
||||
@@ -138,13 +138,13 @@ describe('scheduleBackgroundJobFactory', () => {
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const scheduleBackgroundJob = scheduleBackgroundJobFactory({
|
||||
const createBackgroundJob = createBackgroundJobFactory({
|
||||
storeBackgroundJob: failingStore,
|
||||
jobConfig: mockJobConfig
|
||||
})
|
||||
|
||||
try {
|
||||
await scheduleBackgroundJob({ jobPayload: mockJobPayload })
|
||||
await createBackgroundJob({ jobPayload: mockJobPayload })
|
||||
expect.fail('Should have thrown an error')
|
||||
} catch (error) {
|
||||
expect(error).to.be.instanceof(Error)
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '@/modules/fileuploads/domain/consts'
|
||||
import type { Knex } from 'knex'
|
||||
import { migrateDbToLatest } from '@/db/migrations'
|
||||
import { scheduleBackgroundJobFactory } from '@/modules/backgroundjobs/services'
|
||||
import { createBackgroundJobFactory } from '@/modules/backgroundjobs/services/create'
|
||||
import {
|
||||
getBackgroundJobCountFactory,
|
||||
storeBackgroundJobFactory
|
||||
@@ -136,7 +136,7 @@ export const initializePostgresQueue = async ({
|
||||
// migrating the DB up, the queue DB might be added based on a config
|
||||
await migrateDbToLatest({ db, region: `Queue DB for ${label}` })
|
||||
|
||||
const scheduleBackgroundJob = scheduleBackgroundJobFactory({
|
||||
const createBackgroundJob = createBackgroundJobFactory({
|
||||
jobConfig: { maxAttempt: 3, timeoutMs: timeout },
|
||||
storeBackgroundJob: storeBackgroundJobFactory({
|
||||
db,
|
||||
@@ -152,7 +152,7 @@ export const initializePostgresQueue = async ({
|
||||
),
|
||||
shutdown: async () => {},
|
||||
scheduleJob: async (jobData: JobPayload) => {
|
||||
await scheduleBackgroundJob({
|
||||
await createBackgroundJob({
|
||||
jobPayload: { jobType: 'fileImport', payloadVersion: 1, ...jobData }
|
||||
})
|
||||
},
|
||||
|
||||
@@ -186,7 +186,7 @@ export default class Sandbox {
|
||||
this.addStreamControls(url)
|
||||
this.addViewControls()
|
||||
this.addBatches()
|
||||
this.properties = await this.viewer.getObjectProperties()
|
||||
// this.properties = await this.viewer.getObjectProperties()
|
||||
this.batchesParams.totalBvhSize = this.getBVHSize()
|
||||
this.refresh()
|
||||
})
|
||||
|
||||
@@ -135,6 +135,7 @@ const getStream = () => {
|
||||
// prettier-ignore
|
||||
// Revit sample house (good for bim-like stuff with many display meshes)
|
||||
'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
|
||||
// 'https://app.speckle.systems/streams/da9e320dad/objects/ee5d160d84090822813bc74188da34a7'
|
||||
|
||||
//large tower
|
||||
//'https://app.speckle.systems/projects/e2a7b596f2/models/ddaf8349f5'
|
||||
@@ -162,6 +163,7 @@ const getStream = () => {
|
||||
// 'https://latest.speckle.systems/streams/3ed8357f29/commits/d10f2af1ce'
|
||||
// AutoCAD NEW
|
||||
// 'https://latest.speckle.systems/streams/3ed8357f29/commits/46905429f6'
|
||||
// 'https://latest.speckle.systems/streams/3ed8357f29/objects/95160b8d593a0ba12dd004d5fe142257'
|
||||
//Blizzard world
|
||||
// 'https://latest.speckle.systems/streams/0c6ad366c4/commits/aa1c393aec'
|
||||
//Car
|
||||
@@ -355,7 +357,6 @@ const getStream = () => {
|
||||
// 'https://app.speckle.systems/streams/25d8a162af/commits/6c842a713c'
|
||||
// 'https://app.speckle.systems/streams/76e3acde68/commits/0ea3d47e6c'
|
||||
// Point cloud
|
||||
// 'https://app.speckle.systems/streams/b920636274/commits/8df6496749'
|
||||
// 'https://multiconsult.speckle.xyz/streams/9721fe797c/objects/ff5d939a8c26bde092152d5b4a0c945d'
|
||||
// 'https://app.speckle.systems/streams/87a2be92c7/objects/803c3c413b133ee9a6631160ccb194c9'
|
||||
// 'https://latest.speckle.systems/streams/1422d91a81/commits/480d88ba68'
|
||||
@@ -543,7 +544,7 @@ const getStream = () => {
|
||||
// 'https://app.speckle.systems/projects/7591c56179/models/82b94108a3'
|
||||
|
||||
// SUPER slow tree build time (LARGE N-GONS TRIANGULATION)
|
||||
// 'https://app.speckle.systems/projects/0edb6ef628/models/ff3d8480bc@cd83d90a2c'
|
||||
// 'https://app.speckle.systems/projects/0edb6ef628/models/87f3fb5e2bd681d731dd048390ae3a8f'
|
||||
|
||||
/* ObjectLoader 2 tests */
|
||||
// `https://latest.speckle.systems/projects/97750296c2/models/767b70fc63@5386a0af02`
|
||||
@@ -601,6 +602,28 @@ const getStream = () => {
|
||||
// 'https://latest.speckle.systems/projects/f28ad5b38a/models/b63ebcd807'
|
||||
// Duplicate display values
|
||||
// 'https://app.speckle.systems/projects/1466fe31c6/models/2eaf0f0571'
|
||||
// MEPS
|
||||
// 'https://app.speckle.systems/projects/f3cee517d4/models/21f128a3ea'
|
||||
// Tower
|
||||
// 'https://app.speckle.systems/projects/e2a7b596f2/models/ddaf8349f5'
|
||||
|
||||
// Barbican
|
||||
// 'https://app.speckle.systems/projects/32baa9291e/models/all'
|
||||
// 'https://app.speckle.systems/streams/32baa9291e/objects/21a3621c0a3e6d2884e1315f02314313'
|
||||
// 'https://app.speckle.systems/projects/5d723f097a/models/c05abd36b5'
|
||||
|
||||
//Guggenheim
|
||||
// 'https://app.speckle.systems/projects/937d78e0a5/models/a48f0274eb'
|
||||
// 'https://app.speckle.systems/projects/937d78e0a5/objects/0e3c61147f3a035a85a3542c7f1c7a43'
|
||||
|
||||
// heatherwick LARGE
|
||||
// 'https://app.speckle.systems/projects/63a3226049/models/bdd4f553a8'
|
||||
|
||||
// Mirrored instances
|
||||
// 'https://app.speckle.systems/projects/b6e95c0c63/models/024ce31c6f@a66c3956d6'
|
||||
|
||||
// Text with color proxy
|
||||
// 'https://app.speckle.systems/projects/ebf93a561b/models/0a07bc3231'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ export type SpeckleObject = {
|
||||
applicationId?: string
|
||||
}
|
||||
|
||||
export type DataChunk = {
|
||||
id: string
|
||||
data: number[]
|
||||
references: number
|
||||
processed?: boolean
|
||||
}
|
||||
|
||||
export interface ViewerParams {
|
||||
showStats: boolean
|
||||
environmentSrc: Asset
|
||||
|
||||
@@ -299,9 +299,10 @@ async function runAllModelsQuery(
|
||||
const urls: string[] = []
|
||||
data.project.models.items.forEach(
|
||||
(element: { versions: { items: { referencedObject: string }[] } }) => {
|
||||
urls.push(
|
||||
`${ref.origin}/streams/${ref.projectId}/objects/${element.versions.items[0].referencedObject}`
|
||||
)
|
||||
if (element.versions.items.length)
|
||||
urls.push(
|
||||
`${ref.origin}/streams/${ref.projectId}/objects/${element.versions.items[0].referencedObject}`
|
||||
)
|
||||
}
|
||||
)
|
||||
return urls
|
||||
|
||||
@@ -102,8 +102,16 @@ export class BatchObject {
|
||||
this.pivot_High
|
||||
)
|
||||
}
|
||||
public buildAccelerationStructure(
|
||||
position: Float32Array | Float64Array,
|
||||
indices: Uint16Array | Uint32Array
|
||||
): void
|
||||
public buildAccelerationStructure(bvh: MeshBVH): void
|
||||
|
||||
public buildAccelerationStructure(bvh?: MeshBVH) {
|
||||
public buildAccelerationStructure(
|
||||
positionOrBvh: Float32Array | Float64Array | MeshBVH,
|
||||
indices?: Uint16Array | Uint32Array
|
||||
): void {
|
||||
const transform = new Matrix4().makeTranslation(
|
||||
this._localOrigin.x,
|
||||
this._localOrigin.y,
|
||||
@@ -111,14 +119,15 @@ export class BatchObject {
|
||||
)
|
||||
transform.invert()
|
||||
|
||||
if (!bvh) {
|
||||
const indices: number[] | undefined =
|
||||
this._renderView.renderData.geometry.attributes?.INDEX
|
||||
const position: number[] | undefined =
|
||||
this._renderView.renderData.geometry.attributes?.POSITION
|
||||
let bvh = positionOrBvh
|
||||
|
||||
if (!(bvh instanceof MeshBVH)) {
|
||||
if (!indices) {
|
||||
throw new Error(`Cannot build a BVH with only positions. Need indices too`)
|
||||
}
|
||||
bvh = AccelerationStructure.buildBVH(
|
||||
indices,
|
||||
position,
|
||||
positionOrBvh as Float32Array | Float64Array,
|
||||
DefaultBVHOptions,
|
||||
transform
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ import SpeckleMesh, { TransformStorage } from '../objects/SpeckleMesh.js'
|
||||
import { SpeckleType } from '../loaders/GeometryConverter.js'
|
||||
import { type TreeNode, WorldTree } from '../../index.js'
|
||||
import { InstancedMeshBatch } from './InstancedMeshBatch.js'
|
||||
import { Geometry } from '../converter/Geometry.js'
|
||||
import { Geometry, GeometryData } from '../converter/Geometry.js'
|
||||
import { MeshBatch } from './MeshBatch.js'
|
||||
import { PointBatch } from './PointBatch.js'
|
||||
import Logger from '../utils/Logger.js'
|
||||
@@ -139,23 +139,27 @@ export default class Batcher {
|
||||
needsRTE
|
||||
) {
|
||||
rvs.forEach((nodeRv) => {
|
||||
const geometry = nodeRv.renderData.geometry
|
||||
const geometry = nodeRv.renderData.geometry as GeometryData
|
||||
geometry.instanced = false
|
||||
const attribs = geometry.attributes
|
||||
geometry.attributes = {
|
||||
POSITION: attribs.POSITION.slice(),
|
||||
INDEX: attribs.INDEX.slice(),
|
||||
...(attribs.COLOR && {
|
||||
COLOR: attribs.COLOR.slice()
|
||||
})
|
||||
}
|
||||
/** - I don't particularly like this branch -
|
||||
* All instances should have a transform. But it's the easiest thing we can do
|
||||
* until we figure out the viewer <-> connector object duplication inconsistency
|
||||
*/
|
||||
if (geometry.transform)
|
||||
Geometry.transformGeometryData(geometry, geometry.transform)
|
||||
nodeRv.computeAABB()
|
||||
nodeRv.computeAABB(geometry.transform)
|
||||
if ((geometry.transform?.determinant() ?? 0) < 0)
|
||||
geometry.flipNormals = true
|
||||
/** I don't think we need to duplicate geometry here, now that we're transforming the batch position directly */
|
||||
// const attribs = geometry.attributes
|
||||
// geometry.attributes = {
|
||||
// POSITION: attribs.POSITION.slice(),
|
||||
// INDEX: attribs.INDEX.slice(),
|
||||
// ...(attribs.COLOR && {
|
||||
// COLOR: attribs.COLOR.slice()
|
||||
// })
|
||||
// }
|
||||
// /** - I don't particularly like this branch -
|
||||
// * All instances should have a transform. But it's the easiest thing we can do
|
||||
// * until we figure out the viewer <-> connector object duplication inconsistency
|
||||
// */
|
||||
// if (geometry.transform)
|
||||
// Geometry.transformGeometryData(geometry, geometry.transform)
|
||||
// nodeRv.computeAABB()
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -383,7 +387,9 @@ export default class Batcher {
|
||||
} else if (geometryType === GeometryType.POINT_CLOUD) {
|
||||
matRef = renderViews[0].renderData.renderMaterial
|
||||
} else if (geometryType === GeometryType.TEXT) {
|
||||
matRef = renderViews[0].renderData.displayStyle
|
||||
matRef = renderViews[0].renderData.colorMaterial
|
||||
? renderViews[0].renderData.colorMaterial
|
||||
: renderViews[0].renderData.displayStyle
|
||||
}
|
||||
|
||||
const material = this.materials.getMaterial(materialHash, matRef, geometryType)
|
||||
|
||||
@@ -525,6 +525,21 @@ export class InstancedMeshBatch implements Batch {
|
||||
)
|
||||
const targetInstanceTransformBuffer = this.getCurrentTransformBuffer()
|
||||
|
||||
const positions =
|
||||
this.renderViews[0].renderData.geometry.attributes?.POSITION.getFloat32Array()
|
||||
const indicesLength =
|
||||
this.renderViews[0].renderData.geometry.attributes?.INDEX.length ?? 0
|
||||
const positionsLength =
|
||||
this.renderViews[0].renderData.geometry.attributes?.POSITION.length ?? 0
|
||||
const indices =
|
||||
indicesLength < 65535 && positionsLength < 65535
|
||||
? this.renderViews[0].renderData.geometry.attributes?.INDEX.getUint16Array()
|
||||
: this.renderViews[0].renderData.geometry.attributes?.INDEX.getUint32Array()
|
||||
const colors =
|
||||
this.renderViews[0].renderData.geometry.attributes?.COLOR?.getFloat32Array()
|
||||
const normals =
|
||||
this.renderViews[0].renderData.geometry.attributes?.NORMAL?.getFloat32Array()
|
||||
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
/** Catering to typescript
|
||||
* There is no unniverse where an instanced render view does not have a transform
|
||||
@@ -555,13 +570,10 @@ export class InstancedMeshBatch implements Batch {
|
||||
batchObject.localOrigin.z
|
||||
)
|
||||
transform.invert()
|
||||
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,
|
||||
position,
|
||||
positions,
|
||||
DefaultBVHOptions,
|
||||
transform
|
||||
)
|
||||
@@ -572,32 +584,13 @@ export class InstancedMeshBatch implements Batch {
|
||||
batchObjects.push(batchObject)
|
||||
}
|
||||
|
||||
const indices: number[] | undefined =
|
||||
this.renderViews[0].renderData.geometry.attributes?.INDEX
|
||||
|
||||
const positions: number[] | undefined =
|
||||
this.renderViews[0].renderData.geometry.attributes?.POSITION
|
||||
|
||||
const colors: number[] | undefined =
|
||||
this.renderViews[0].renderData.geometry.attributes?.COLOR
|
||||
|
||||
const normals: number[] | undefined =
|
||||
this.renderViews[0].renderData.geometry.attributes?.NORMAL
|
||||
|
||||
/** 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.geometry = this.makeInstancedMeshGeometry(
|
||||
positions.length >= 65535 || indices.length >= 65535
|
||||
? new Uint32Array(indices)
|
||||
: new Uint16Array(indices),
|
||||
new Float64Array(positions),
|
||||
normals ? new Float32Array(normals) : undefined,
|
||||
colors ? new Float32Array(colors) : undefined
|
||||
)
|
||||
this.geometry = this.makeInstancedMeshGeometry(indices, positions, normals, colors)
|
||||
|
||||
this.mesh = new SpeckleInstancedMesh(this.geometry)
|
||||
this.mesh.setBatchObjects(batchObjects)
|
||||
@@ -655,16 +648,12 @@ export class InstancedMeshBatch implements Batch {
|
||||
|
||||
private makeInstancedMeshGeometry(
|
||||
indices: Uint32Array | Uint16Array,
|
||||
position: Float64Array,
|
||||
position: Float32Array,
|
||||
normal?: Float32Array,
|
||||
color?: Float32Array
|
||||
): BufferGeometry {
|
||||
const geometry = new BufferGeometry()
|
||||
if (position) {
|
||||
/** When RTE enabled, we'll be storing the high component of the encoding here,
|
||||
* which considering our current encoding method is actually the original casted
|
||||
* down float32 position!
|
||||
*/
|
||||
geometry.setAttribute('position', new Float32BufferAttribute(position, 3))
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from './Batch.js'
|
||||
import { ObjectLayers } from '../../IViewer.js'
|
||||
import Materials from '../materials/Materials.js'
|
||||
import { ChunkArray } from '../converter/VirtualArray.js'
|
||||
|
||||
export default class LineBatch implements Batch {
|
||||
public id: string
|
||||
@@ -231,6 +232,7 @@ export default class LineBatch implements Batch {
|
||||
|
||||
public buildBatch() {
|
||||
let attributeCount = 0
|
||||
const rvAABB: Box3 = new Box3()
|
||||
const bounds = new Box3()
|
||||
this.renderViews.forEach((val: NodeRenderView) => {
|
||||
if (!val.renderData.geometry.attributes) {
|
||||
@@ -241,14 +243,18 @@ export default class LineBatch implements Batch {
|
||||
: val.renderData.geometry.attributes.POSITION.length
|
||||
bounds.union(val.aabb)
|
||||
})
|
||||
const position = new Float64Array(attributeCount)
|
||||
const needsRTE = Geometry.needsRTE(bounds)
|
||||
|
||||
const position = needsRTE
|
||||
? new Float64Array(attributeCount)
|
||||
: new Float32Array(attributeCount)
|
||||
let offset = 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`)
|
||||
}
|
||||
let points: Array<number>
|
||||
let points: Array<number> | ChunkArray
|
||||
/** 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
|
||||
@@ -258,19 +264,30 @@ export default class LineBatch implements Batch {
|
||||
points = new Array(2 * length)
|
||||
|
||||
for (let i = 0; i < length; i += 3) {
|
||||
points[2 * i] = geometry.attributes.POSITION[i]
|
||||
points[2 * i + 1] = geometry.attributes.POSITION[i + 1]
|
||||
points[2 * i + 2] = geometry.attributes.POSITION[i + 2]
|
||||
points[2 * i] = geometry.attributes.POSITION.get(i)
|
||||
points[2 * i + 1] = geometry.attributes.POSITION.get(i + 1)
|
||||
points[2 * i + 2] = geometry.attributes.POSITION.get(i + 2)
|
||||
|
||||
points[2 * i + 3] = geometry.attributes.POSITION[i + 3]
|
||||
points[2 * i + 4] = geometry.attributes.POSITION[i + 4]
|
||||
points[2 * i + 5] = geometry.attributes.POSITION[i + 5]
|
||||
points[2 * i + 3] = geometry.attributes.POSITION.get(i + 3)
|
||||
points[2 * i + 4] = geometry.attributes.POSITION.get(i + 4)
|
||||
points[2 * i + 5] = geometry.attributes.POSITION.get(i + 5)
|
||||
}
|
||||
position.set(points, offset)
|
||||
} else {
|
||||
points = geometry.attributes.POSITION
|
||||
geometry.attributes.POSITION.copyToBuffer(position, offset)
|
||||
}
|
||||
|
||||
position.set(points, offset)
|
||||
const positionSubArray = position.subarray(offset, offset + points.length)
|
||||
Geometry.transformArray(positionSubArray, geometry.transform, 0, points.length)
|
||||
/** We re-compute the render view aabb based on transformed geometry
|
||||
* We do this because some transforms like non-uniform scaling can produce incorrect results
|
||||
* if we compute an aabb from original geometry then apply the transform. That's why we compute
|
||||
* an aabb from the transformed geometry here and set it in the rv
|
||||
*/
|
||||
rvAABB.setFromArray(positionSubArray)
|
||||
this.renderViews[k].aabb = rvAABB
|
||||
|
||||
this.renderViews[k].setBatchData(this.id, offset / 6, points.length / 6)
|
||||
|
||||
offset += points.length
|
||||
@@ -319,10 +336,15 @@ export default class LineBatch implements Batch {
|
||||
return material
|
||||
}
|
||||
|
||||
private makeLineGeometry(position: Float64Array): LineSegmentsGeometry {
|
||||
private makeLineGeometry(
|
||||
position: Float64Array | Float32Array
|
||||
): LineSegmentsGeometry {
|
||||
const geometry = new LineSegmentsGeometry()
|
||||
/** This will set the instanceStart and instanceEnd attributes. These will be our high parts */
|
||||
geometry.setPositions(new Float32Array(position))
|
||||
if (position instanceof Float64Array)
|
||||
/** We need to re-allocate because there is no way to cast it down to float32. If we pass in a Float64Array, three.js will do it anyway */
|
||||
geometry.setPositions(new Float32Array(position))
|
||||
else geometry.setPositions(position)
|
||||
|
||||
const buffer = new Float32Array(position.length + position.length / 3)
|
||||
this.colorBuffer = new InstancedInterleavedBuffer(buffer, 8, 1) // rgba, rgba
|
||||
|
||||
@@ -193,6 +193,7 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
public buildBatch(): Promise<void> {
|
||||
let indicesCount = 0
|
||||
let attributeCount = 0
|
||||
const rvAABB: Box3 = new Box3()
|
||||
const bounds: Box3 = new Box3()
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
const ervee = this.renderViews[k]
|
||||
@@ -209,11 +210,17 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
attributeCount += ervee.renderData.geometry.attributes.POSITION.length
|
||||
bounds.union(ervee.aabb)
|
||||
}
|
||||
const needsRTE = Geometry.needsRTE(bounds)
|
||||
|
||||
const hasVertexColors =
|
||||
this.renderViews[0].renderData.geometry.attributes?.COLOR !== undefined
|
||||
const indices = new Uint32Array(indicesCount)
|
||||
const position = new Float64Array(attributeCount)
|
||||
const indices =
|
||||
attributeCount >= 65535 || indicesCount >= 65535
|
||||
? new Uint32Array(indicesCount)
|
||||
: new Uint16Array(indicesCount)
|
||||
const position = needsRTE
|
||||
? new Float64Array(attributeCount)
|
||||
: new Float32Array(attributeCount)
|
||||
const color = new Float32Array(hasVertexColors ? attributeCount : 0)
|
||||
color.fill(1)
|
||||
const batchIndices = new Float32Array(attributeCount / 3)
|
||||
@@ -228,49 +235,90 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
/** Catering to typescript
|
||||
* There is no unniverse where indices or positions are undefined at this point
|
||||
*/
|
||||
if (!geometry.attributes || !geometry.attributes.INDEX) {
|
||||
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
|
||||
|
||||
geometry.attributes?.INDEX.copyToBuffer(indices, arrayOffset)
|
||||
const indicesSubArray = indices.subarray(
|
||||
arrayOffset,
|
||||
arrayOffset + geometry.attributes?.INDEX.length
|
||||
)
|
||||
position.set(geometry.attributes.POSITION, offset)
|
||||
if (geometry.attributes.COLOR) color.set(geometry.attributes.COLOR, offset)
|
||||
|
||||
geometry.attributes?.POSITION.copyToBuffer(position, offset)
|
||||
const positionSubarray = position.subarray(
|
||||
offset,
|
||||
offset +
|
||||
(this.renderViews[k].renderData.geometry.attributes?.POSITION.length ?? 0)
|
||||
)
|
||||
/** We transform the copied geometry so that we do not alter original chunk data which might be shared */
|
||||
Geometry.transformArray(
|
||||
positionSubarray,
|
||||
geometry.transform,
|
||||
0,
|
||||
geometry.attributes?.POSITION.length
|
||||
)
|
||||
|
||||
if (geometry.attributes.COLOR) {
|
||||
geometry.attributes?.COLOR.copyToBuffer(color, offset)
|
||||
}
|
||||
|
||||
/** We either copy over the provided vertex normals */
|
||||
if (geometry.attributes.NORMAL) {
|
||||
normals.set(geometry.attributes.NORMAL, offset)
|
||||
geometry.attributes?.NORMAL.copyToBuffer(normals, offset)
|
||||
if (geometry.flipNormals) {
|
||||
Geometry.flipNormalsBuffer(
|
||||
normals.subarray(offset, offset + geometry.attributes?.NORMAL.length)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
/** Either we compute them ourselves */
|
||||
Geometry.computeVertexNormalsBuffer(
|
||||
Geometry.computeVertexNormalsBufferVirtual(
|
||||
normals.subarray(
|
||||
offset,
|
||||
offset + geometry.attributes.POSITION.length
|
||||
offset +
|
||||
(this.renderViews[k].renderData.geometry.attributes?.POSITION.length ?? 0)
|
||||
) as unknown as number[],
|
||||
geometry.attributes.POSITION,
|
||||
geometry.attributes.INDEX
|
||||
geometry.attributes.INDEX,
|
||||
geometry.flipNormals
|
||||
)
|
||||
}
|
||||
batchIndices.fill(
|
||||
k,
|
||||
offset / 3,
|
||||
offset / 3 + geometry.attributes.POSITION.length / 3
|
||||
)
|
||||
batchIndices.fill(k, offset / 3, offset / 3 + geometry.attributes.POSITION.length)
|
||||
this.renderViews[k].setBatchData(
|
||||
this.id,
|
||||
arrayOffset,
|
||||
geometry.attributes.INDEX.length,
|
||||
offset / 3,
|
||||
offset / 3 + geometry.attributes.POSITION.length / 3
|
||||
offset / 3 + geometry.attributes.POSITION.length
|
||||
)
|
||||
|
||||
/** We re-compute the render view aabb based on transformed geometry
|
||||
* We do this because some transforms like non-uniform scaling can produce incorrect results
|
||||
* if we compute an aabb from original geometry then apply the transform. That's why we compute
|
||||
* an aabb from the transformed geometry here and set it in the rv
|
||||
*/
|
||||
rvAABB.setFromArray(positionSubarray)
|
||||
this.renderViews[k].aabb = rvAABB
|
||||
|
||||
const batchObject = new BatchObject(this.renderViews[k], k)
|
||||
batchObject.buildAccelerationStructure()
|
||||
batchObject.buildAccelerationStructure(positionSubarray, indicesSubArray)
|
||||
batchObjects.push(batchObject)
|
||||
|
||||
indices.set(
|
||||
batchObject.accelerationStructure.bvh.geometry.index?.array as number[],
|
||||
arrayOffset
|
||||
)
|
||||
|
||||
/** Re-index the indices inside the batch */
|
||||
for (let i = 0; i < indicesSubArray.length; i++) {
|
||||
indicesSubArray[i] = indicesSubArray[i] + offset / 3
|
||||
}
|
||||
|
||||
offset += geometry.attributes.POSITION.length
|
||||
arrayOffset += geometry.attributes.INDEX.length
|
||||
|
||||
this.renderViews[k].disposeGeometry()
|
||||
}
|
||||
|
||||
const geometry = this.makeMeshGeometry(
|
||||
@@ -280,7 +328,7 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
batchIndices,
|
||||
hasVertexColors ? color : undefined
|
||||
)
|
||||
const needsRTE = Geometry.needsRTE(bounds)
|
||||
|
||||
if (needsRTE) Geometry.updateRTEGeometry(geometry, position)
|
||||
|
||||
this.primitive = new SpeckleMesh(geometry, needsRTE)
|
||||
@@ -296,16 +344,16 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
this.primitive.frustumCulled = false
|
||||
this.primitive.geometry.addGroup(0, this.getCount(), 0)
|
||||
|
||||
batchObjects.forEach((element: BatchObject) => {
|
||||
element.renderView.disposeGeometry()
|
||||
})
|
||||
// batchObjects.forEach((element: BatchObject) => {
|
||||
// element.renderView.disposeGeometry()
|
||||
// })
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
protected makeMeshGeometry(
|
||||
indices: Uint32Array | Uint16Array,
|
||||
position: Float64Array,
|
||||
position: Float64Array | Float32Array,
|
||||
normals: Float32Array,
|
||||
batchIndices: Float32Array,
|
||||
color?: Float32Array
|
||||
@@ -313,10 +361,10 @@ export class MeshBatch extends PrimitiveBatch {
|
||||
const geometry = new BufferGeometry()
|
||||
if (position.length >= 65535 || indices.length >= 65535) {
|
||||
this.indexBuffer0 = new Uint32BufferAttribute(indices, 1)
|
||||
this.indexBuffer1 = new Uint32BufferAttribute(new Uint32Array(indices.length), 1)
|
||||
this.indexBuffer1 = new Uint32BufferAttribute(indices, 1)
|
||||
} else {
|
||||
this.indexBuffer0 = new Uint16BufferAttribute(indices, 1)
|
||||
this.indexBuffer1 = new Uint16BufferAttribute(new Uint16Array(indices.length), 1)
|
||||
this.indexBuffer1 = new Uint16BufferAttribute(indices, 1)
|
||||
}
|
||||
geometry.setIndex(this.indexBuffer0)
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ export class PointBatch extends PrimitiveBatch {
|
||||
|
||||
public buildBatch(): Promise<void> {
|
||||
let attributeCount = 0
|
||||
const rvAABB: Box3 = new Box3()
|
||||
const bounds = new Box3()
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
const ervee = this.renderViews[k]
|
||||
@@ -170,9 +171,17 @@ export class PointBatch extends PrimitiveBatch {
|
||||
attributeCount += ervee.renderData.geometry.attributes.POSITION.length
|
||||
bounds.union(ervee.aabb)
|
||||
}
|
||||
const position = new Float64Array(attributeCount)
|
||||
const color = new Float32Array(attributeCount).fill(1)
|
||||
const index = new Int32Array(attributeCount / 3)
|
||||
const needsRTE = Geometry.needsRTE(bounds)
|
||||
const hasVertexColors =
|
||||
this.renderViews[0].renderData.geometry.attributes?.COLOR !== undefined
|
||||
const needsInt32Indices = attributeCount >= 65535
|
||||
const position = needsRTE
|
||||
? new Float64Array(attributeCount)
|
||||
: new Float32Array(attributeCount)
|
||||
const color = new Float32Array(hasVertexColors ? attributeCount : 0).fill(1)
|
||||
const index = needsInt32Indices
|
||||
? new Uint32Array(attributeCount / 3)
|
||||
: new Uint16Array(attributeCount / 3)
|
||||
let offset = 0
|
||||
let indexOffset = 0
|
||||
for (let k = 0; k < this.renderViews.length; k++) {
|
||||
@@ -180,14 +189,39 @@ export class PointBatch extends PrimitiveBatch {
|
||||
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)
|
||||
|
||||
geometry.attributes?.POSITION.copyToBuffer(position, offset)
|
||||
const positionSubarray = position.subarray(
|
||||
offset,
|
||||
offset +
|
||||
(this.renderViews[k].renderData.geometry.attributes?.POSITION.length ?? 0)
|
||||
)
|
||||
|
||||
Geometry.transformArray(
|
||||
positionSubarray,
|
||||
geometry.transform,
|
||||
0,
|
||||
geometry.attributes?.POSITION.length
|
||||
)
|
||||
if (geometry.attributes.COLOR) {
|
||||
geometry.attributes?.COLOR.copyToBuffer(color, offset)
|
||||
}
|
||||
index.set(
|
||||
new Int32Array(geometry.attributes.POSITION.length / 3).map(
|
||||
(_value, index) => index + indexOffset
|
||||
),
|
||||
(needsInt32Indices
|
||||
? new Uint32Array(geometry.attributes.POSITION.length / 3)
|
||||
: new Uint16Array(geometry.attributes.POSITION.length / 3)
|
||||
).map((_value, index) => index + indexOffset),
|
||||
indexOffset
|
||||
)
|
||||
|
||||
/** We re-compute the render view aabb based on transformed geometry
|
||||
* We do this because some transforms like non-uniform scaling can produce incorrect results
|
||||
* if we compute an aabb from original geometry then apply the transform. That's why we compute
|
||||
* an aabb from the transformed geometry here and set it in the rv
|
||||
*/
|
||||
rvAABB.setFromArray(positionSubarray)
|
||||
this.renderViews[k].aabb = rvAABB
|
||||
|
||||
this.renderViews[k].setBatchData(
|
||||
this.id,
|
||||
offset / 3,
|
||||
@@ -201,7 +235,7 @@ export class PointBatch extends PrimitiveBatch {
|
||||
}
|
||||
const geometry = this.makePointGeometry(index, position, color)
|
||||
|
||||
if (Geometry.needsRTE(bounds)) {
|
||||
if (needsRTE) {
|
||||
Geometry.updateRTEGeometry(geometry, position)
|
||||
if (!this.batchMaterial.defines) this.batchMaterial.defines = {}
|
||||
this.batchMaterial.defines['USE_RTE'] = ' '
|
||||
@@ -221,8 +255,8 @@ export class PointBatch extends PrimitiveBatch {
|
||||
}
|
||||
|
||||
protected makePointGeometry(
|
||||
index: Int32Array,
|
||||
position: Float64Array,
|
||||
index: Uint32Array | Uint16Array,
|
||||
position: Float64Array | Float32Array,
|
||||
color: Float32Array
|
||||
): BufferGeometry {
|
||||
const geometry = new BufferGeometry()
|
||||
|
||||
@@ -144,8 +144,8 @@ export default class TextBatch implements Batch {
|
||||
* - Even if, the **text batch does not use the materials in it's draw groups**, it emulates the behavior as if it would
|
||||
*/
|
||||
public setBatchBuffers(ranges: BatchUpdateRange[]): void {
|
||||
console.warn(' Groups -> ', this.mesh.groups)
|
||||
console.warn(' Ranges -> ', ranges)
|
||||
// console.warn(' Groups -> ', this.mesh.groups)
|
||||
// console.warn(' Ranges -> ', ranges)
|
||||
const splitRanges: BatchUpdateRange[] = []
|
||||
ranges.forEach((range: BatchUpdateRange) => {
|
||||
for (let k = 0; k < range.count; k++) {
|
||||
@@ -321,7 +321,7 @@ export default class TextBatch implements Batch {
|
||||
screenOriented: boolean
|
||||
}
|
||||
const text = new Text()
|
||||
this.renderViews[k].renderData.geometry.bakeTransform?.decompose(
|
||||
this.renderViews[k].renderData.geometry.transform?.decompose(
|
||||
text.position,
|
||||
text.quaternion,
|
||||
text.scale
|
||||
@@ -349,24 +349,23 @@ export default class TextBatch implements Batch {
|
||||
/** We're using visibleBounds for a better fit */
|
||||
const bounds = textRenderInfo.visibleBounds
|
||||
// console.log('bounds -> ', bounds)
|
||||
const vertices = []
|
||||
vertices.push(
|
||||
bounds[0],
|
||||
bounds[3],
|
||||
0,
|
||||
bounds[2],
|
||||
bounds[3],
|
||||
0,
|
||||
bounds[0],
|
||||
bounds[1],
|
||||
0,
|
||||
bounds[2],
|
||||
bounds[1],
|
||||
0
|
||||
)
|
||||
const vertices = new Float32Array(12)
|
||||
vertices[0] = bounds[0]
|
||||
vertices[1] = bounds[3]
|
||||
vertices[2] = 0
|
||||
vertices[3] = bounds[2]
|
||||
vertices[4] = bounds[3]
|
||||
vertices[5] = 0
|
||||
vertices[6] = bounds[0]
|
||||
vertices[7] = bounds[1]
|
||||
vertices[8] = 0
|
||||
vertices[9] = bounds[2]
|
||||
vertices[10] = bounds[1]
|
||||
vertices[11] = 0
|
||||
|
||||
box.setFromArray(vertices)
|
||||
box.applyMatrix4(
|
||||
this.renderViews[k].renderData.geometry.bakeTransform || new Matrix4()
|
||||
this.renderViews[k].renderData.geometry.transform || new Matrix4()
|
||||
)
|
||||
|
||||
needsRTE ||= Geometry.needsRTE(box)
|
||||
@@ -374,7 +373,7 @@ export default class TextBatch implements Batch {
|
||||
const geometry = text.geometry
|
||||
geometry.computeBoundingBox()
|
||||
const textBvh = AccelerationStructure.buildBVH(
|
||||
geometry.index?.array as number[],
|
||||
geometry.index?.array as unknown as Uint16Array | Uint32Array,
|
||||
vertices,
|
||||
DefaultBVHOptions
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ export class TextBatchObject extends BatchObject {
|
||||
|
||||
public constructor(renderView: NodeRenderView, batchIndex: number) {
|
||||
super(renderView, batchIndex)
|
||||
if (renderView.renderData.geometry.bakeTransform)
|
||||
this.textTransform.copy(renderView.renderData.geometry.bakeTransform)
|
||||
if (renderView.renderData.geometry.transform)
|
||||
this.textTransform.copy(renderView.renderData.geometry.transform)
|
||||
/** TO DO: Not sure we should do this */
|
||||
this.transform.copy(this.textTransform)
|
||||
this.transformInv.copy(new Matrix4().copy(this.textTransform).invert())
|
||||
|
||||
@@ -6,18 +6,19 @@ import {
|
||||
Float32BufferAttribute,
|
||||
InstancedInterleavedBuffer,
|
||||
InterleavedBufferAttribute,
|
||||
MathUtils,
|
||||
Matrix4,
|
||||
Vector2,
|
||||
Vector3,
|
||||
Vector4
|
||||
} from 'three'
|
||||
import { type SpeckleObject } from '../../IViewer.js'
|
||||
import { DataChunk, type SpeckleObject } from '../../IViewer.js'
|
||||
import { getRelativeOffset, makePerspectiveProjection } from '../Helpers.js'
|
||||
import earcut from 'earcut'
|
||||
import { ChunkArray } from './VirtualArray.js'
|
||||
|
||||
const vecBuff0: Vector3 = new Vector3()
|
||||
const floatArrayBuff: Float32Array = new Float32Array(16)
|
||||
Vector3
|
||||
|
||||
export enum GeometryAttributes {
|
||||
POSITION = 'POSITION',
|
||||
@@ -28,18 +29,28 @@ export enum GeometryAttributes {
|
||||
INDEX = 'INDEX'
|
||||
}
|
||||
|
||||
type AttributeValue = ChunkArray
|
||||
|
||||
// Required keys
|
||||
type RequiredKeys = GeometryAttributes.POSITION | GeometryAttributes.INDEX
|
||||
|
||||
// Optional keys
|
||||
type OptionalKeys = Exclude<GeometryAttributes, RequiredKeys>
|
||||
|
||||
// Final shape: required + optional keys
|
||||
type GeometryAttributesShape = {
|
||||
[K in RequiredKeys]: AttributeValue
|
||||
} & {
|
||||
[K in OptionalKeys]?: AttributeValue
|
||||
}
|
||||
|
||||
export interface GeometryData {
|
||||
attributes:
|
||||
| ({
|
||||
[GeometryAttributes.POSITION]: number[]
|
||||
} & Partial<
|
||||
Record<Exclude<GeometryAttributes, GeometryAttributes.POSITION>, number[]>
|
||||
>)
|
||||
| null
|
||||
attributes: GeometryAttributesShape | null
|
||||
bakeTransform: Matrix4 | null
|
||||
transform: Matrix4 | null
|
||||
metaData?: SpeckleObject
|
||||
instanced?: boolean
|
||||
flipNormals?: boolean
|
||||
}
|
||||
|
||||
export class Geometry {
|
||||
@@ -70,11 +81,7 @@ export class Geometry {
|
||||
|
||||
Geometry.DoubleToHighLowBuffer(doublePositions, position_low, position_high)
|
||||
|
||||
const instanceBufferLow = new InstancedInterleavedBuffer(
|
||||
new Float32Array(position_low),
|
||||
6,
|
||||
1
|
||||
) // xyz, xyz
|
||||
const instanceBufferLow = new InstancedInterleavedBuffer(position_low, 6, 1) // xyz, xyz
|
||||
geometry.setAttribute(
|
||||
'instanceStartLow',
|
||||
new InterleavedBufferAttribute(instanceBufferLow, 3, 0)
|
||||
@@ -87,7 +94,7 @@ export class Geometry {
|
||||
}
|
||||
|
||||
static mergeGeometryAttribute(
|
||||
attributes: (number[] | undefined)[],
|
||||
attributes: AttributeValue[],
|
||||
target: Float32Array | Float64Array
|
||||
): ArrayLike<number> {
|
||||
let offset = 0
|
||||
@@ -96,15 +103,15 @@ export class Geometry {
|
||||
if (!attribute || !target) {
|
||||
throw new Error('Cannot merge geometries. Indices or positions are undefined')
|
||||
}
|
||||
target.set(attribute, offset)
|
||||
attribute.copyToBuffer(target, offset)
|
||||
offset += attribute.length
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
static mergeIndexAttribute(
|
||||
indexAttributes: (number[] | undefined)[],
|
||||
positionAttributes: (number[] | undefined)[]
|
||||
indexAttributes: AttributeValue[],
|
||||
positionAttributes: AttributeValue[]
|
||||
): number[] {
|
||||
let indexOffset = 0
|
||||
const mergedIndex = []
|
||||
@@ -117,7 +124,7 @@ export class Geometry {
|
||||
}
|
||||
|
||||
for (let j = 0; j < index.length; ++j) {
|
||||
mergedIndex.push(index[j] + indexOffset / 3)
|
||||
mergedIndex.push(index.get(j) + indexOffset / 3)
|
||||
}
|
||||
|
||||
indexOffset += positions.length
|
||||
@@ -139,53 +146,59 @@ export class Geometry {
|
||||
Geometry.transformGeometryData(geometries[i], geometries[i].bakeTransform)
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
)
|
||||
const positionAttributes: (number[] | undefined)[] = geometries.map((item) => {
|
||||
if (sampleAttributes && sampleAttributes.INDEX) {
|
||||
const indexAttributes = geometries.map((item: GeometryData) => {
|
||||
/** Catering to typescript */
|
||||
if (!item.attributes) return
|
||||
return item.attributes[GeometryAttributes.POSITION]
|
||||
})
|
||||
return item.attributes.INDEX
|
||||
}) as ChunkArray[]
|
||||
const positionAttributes = geometries.map((item) => {
|
||||
/** Catering to typescript */
|
||||
if (!item.attributes) return
|
||||
return item.attributes.POSITION
|
||||
}) as ChunkArray[]
|
||||
/** o_0 Catering to typescript*/
|
||||
if (mergedGeometry.attributes)
|
||||
mergedGeometry.attributes[GeometryAttributes.INDEX] =
|
||||
Geometry.mergeIndexAttribute(indexAttributes, positionAttributes)
|
||||
mergedGeometry.attributes.INDEX = new ChunkArray([
|
||||
{
|
||||
data: Geometry.mergeIndexAttribute(indexAttributes, positionAttributes),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
for (const k in sampleAttributes) {
|
||||
if (k !== GeometryAttributes.INDEX) {
|
||||
const attributes: (number[] | undefined)[] = geometries.map((item) => {
|
||||
const attributes: ChunkArray[] = geometries.map((item) => {
|
||||
/** Catering to typescript */
|
||||
if (!item.attributes) return
|
||||
return item.attributes[k as GeometryAttributes] as number[]
|
||||
})
|
||||
return item.attributes[k as GeometryAttributes]
|
||||
}) as ChunkArray[]
|
||||
/** 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[]
|
||||
if (mergedGeometry.attributes) {
|
||||
const mergedData = 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[]
|
||||
mergedGeometry.attributes[k as GeometryAttributes] = new ChunkArray([
|
||||
{ data: mergedData, id: MathUtils.generateUUID(), references: 1 }
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,23 +215,76 @@ export class Geometry {
|
||||
if (!geometryData.attributes) return
|
||||
if (!geometryData.attributes.POSITION) return
|
||||
if (!m) return
|
||||
if (Geometry.isMatrix4Identity(m)) return
|
||||
|
||||
const e = m.elements
|
||||
geometryData.attributes.POSITION.chunkArray.forEach((chunk: DataChunk) => {
|
||||
for (let k = 0; k < chunk.data.length; k += 3) {
|
||||
const x = chunk.data[k],
|
||||
y = chunk.data[k + 1],
|
||||
z = chunk.data[k + 2]
|
||||
const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15])
|
||||
|
||||
for (let k = 0; k < geometryData.attributes.POSITION.length; k += 3) {
|
||||
const x = geometryData.attributes.POSITION[k],
|
||||
y = geometryData.attributes.POSITION[k + 1],
|
||||
z = geometryData.attributes.POSITION[k + 2]
|
||||
chunk.data[k] = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w
|
||||
chunk.data[k + 1] = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w
|
||||
chunk.data[k + 2] = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public static transformArray(
|
||||
array: number[] | Float32Array | Float64Array,
|
||||
m: Matrix4 | null,
|
||||
offset?: number,
|
||||
count?: number
|
||||
) {
|
||||
if (!m) return
|
||||
if (Geometry.isMatrix4Identity(m)) return
|
||||
|
||||
const e = m.elements
|
||||
offset = offset || 0
|
||||
count = count || array.length
|
||||
for (let k = offset; k < offset + count; k += 3) {
|
||||
const x = array[k],
|
||||
y = array[k + 1],
|
||||
z = array[k + 2]
|
||||
const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15])
|
||||
|
||||
geometryData.attributes.POSITION[k] = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w
|
||||
geometryData.attributes.POSITION[k + 1] =
|
||||
(e[1] * x + e[5] * y + e[9] * z + e[13]) * w
|
||||
geometryData.attributes.POSITION[k + 2] =
|
||||
(e[2] * x + e[6] * y + e[10] * z + e[14]) * w
|
||||
array[k] = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w
|
||||
array[k + 1] = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w
|
||||
array[k + 2] = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w
|
||||
}
|
||||
}
|
||||
|
||||
public static isMatrix4Identity(matrix: Matrix4) {
|
||||
const e = matrix.elements
|
||||
|
||||
// Check all off-diagonal elements first
|
||||
if (
|
||||
e[1] !== 0 ||
|
||||
e[2] !== 0 ||
|
||||
e[3] !== 0 ||
|
||||
e[4] !== 0 ||
|
||||
e[6] !== 0 ||
|
||||
e[7] !== 0 ||
|
||||
e[8] !== 0 ||
|
||||
e[9] !== 0 ||
|
||||
e[11] !== 0 ||
|
||||
e[12] !== 0 ||
|
||||
e[13] !== 0 ||
|
||||
e[14] !== 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Now check diagonals
|
||||
if (e[0] !== 1 || e[5] !== 1 || e[10] !== 1 || e[15] !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public static unpackColors(int32Colors: number[]): number[] {
|
||||
const colors = new Array<number>(int32Colors.length * 3)
|
||||
for (let i = 0; i < int32Colors.length; i++) {
|
||||
@@ -431,9 +497,82 @@ export class Geometry {
|
||||
}
|
||||
}
|
||||
|
||||
public static computeVertexNormalsBufferVirtual(
|
||||
buffer: number[],
|
||||
position: ChunkArray,
|
||||
index: ChunkArray,
|
||||
flip: boolean = false
|
||||
) {
|
||||
const pA = new Vector3(),
|
||||
pB = new Vector3(),
|
||||
pC = new Vector3()
|
||||
const nA = new Vector3(),
|
||||
nB = new Vector3(),
|
||||
nC = new Vector3()
|
||||
const cb = new Vector3(),
|
||||
ab = new Vector3()
|
||||
|
||||
// indexed elements
|
||||
for (let i = 0, il = index.length; i < il; i += 3) {
|
||||
const vA = index.get(i + 0)
|
||||
const vB = index.get(i + 1)
|
||||
const vC = index.get(i + 2)
|
||||
pA.set(position.get(vA * 3), position.get(vA * 3 + 1), position.get(vA * 3 + 2))
|
||||
pB.set(position.get(vB * 3), position.get(vB * 3 + 1), position.get(vB * 3 + 2))
|
||||
pC.set(position.get(vC * 3), position.get(vC * 3 + 1), position.get(vC * 3 + 2))
|
||||
|
||||
cb.subVectors(pC, pB)
|
||||
ab.subVectors(pA, pB)
|
||||
cb.cross(ab)
|
||||
|
||||
nA.fromArray(buffer, vA * 3)
|
||||
nB.fromArray(buffer, vB * 3)
|
||||
nC.fromArray(buffer, vC * 3)
|
||||
|
||||
nA.add(cb)
|
||||
nB.add(cb)
|
||||
nC.add(cb)
|
||||
|
||||
if (flip) {
|
||||
nA.normalize()
|
||||
nB.normalize()
|
||||
nC.normalize()
|
||||
|
||||
nA.negate()
|
||||
nB.negate()
|
||||
nC.negate()
|
||||
}
|
||||
|
||||
buffer[vA * 3] = nA.x
|
||||
buffer[vA * 3 + 1] = nA.y
|
||||
buffer[vA * 3 + 2] = nA.z
|
||||
|
||||
buffer[vB * 3] = nB.x
|
||||
buffer[vB * 3 + 1] = nB.y
|
||||
buffer[vB * 3 + 2] = nB.z
|
||||
|
||||
buffer[vC * 3] = nC.x
|
||||
buffer[vC * 3 + 1] = nC.y
|
||||
buffer[vC * 3 + 2] = nC.z
|
||||
}
|
||||
}
|
||||
|
||||
// ¯\_(ツ)_/¯
|
||||
public static flipNormalsBuffer(buffer: Float32Array) {
|
||||
const vec3 = new Vector3()
|
||||
for (let k = 0; k < buffer.length; k += 3) {
|
||||
vec3.set(buffer[k], buffer[k + 1], buffer[k + 2])
|
||||
vec3.normalize()
|
||||
vec3.negate()
|
||||
buffer[k] = vec3.x
|
||||
buffer[k + 1] = vec3.y
|
||||
buffer[k + 2] = vec3.z
|
||||
}
|
||||
}
|
||||
|
||||
public static computeVertexNormals(
|
||||
buffer: BufferGeometry,
|
||||
doublePositions: Float64Array
|
||||
positions: Float64Array | Float32Array
|
||||
) {
|
||||
const index = buffer.index
|
||||
const positionAttribute = buffer.getAttribute('position')
|
||||
@@ -470,9 +609,9 @@ export class Geometry {
|
||||
const vB = index.getX(i + 1)
|
||||
const vC = index.getX(i + 2)
|
||||
|
||||
pA.fromArray(doublePositions, vA * 3)
|
||||
pB.fromArray(doublePositions, vB * 3)
|
||||
pC.fromArray(doublePositions, vC * 3)
|
||||
pA.fromArray(positions, vA * 3)
|
||||
pB.fromArray(positions, vB * 3)
|
||||
pC.fromArray(positions, vC * 3)
|
||||
|
||||
cb.subVectors(pC, pB)
|
||||
ab.subVectors(pA, pB)
|
||||
@@ -495,9 +634,9 @@ export class Geometry {
|
||||
|
||||
for (let i = 0, il = positionAttribute.count; i < il; i += 3) {
|
||||
/** This is done blind. Don't think speckle supports non-indexed geometry */
|
||||
pA.fromArray(doublePositions, i * 3)
|
||||
pB.fromArray(doublePositions, i * 3 + 1)
|
||||
pC.fromArray(doublePositions, i * 3 + 2)
|
||||
pA.fromArray(positions, i * 3)
|
||||
pB.fromArray(positions, i * 3 + 1)
|
||||
pC.fromArray(positions, i * 3 + 2)
|
||||
|
||||
cb.subVectors(pC, pB)
|
||||
ab.subVectors(pA, pB)
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { Triangle, Vector3 } from 'three'
|
||||
|
||||
/**
|
||||
* Set of functions to triangulate n-gon faces (i.e. polygon faces with an arbitrary (n) number of vertices).
|
||||
* This class is a JavaScript port of https://github.com/specklesystems/speckle-sharp/blob/main/Objects/Objects/Utils/MeshTriangulationHelper.cs
|
||||
*/
|
||||
const _vec30 = new Vector3()
|
||||
const _vec31 = new Vector3()
|
||||
const _vec32 = new Vector3()
|
||||
const _vec33 = new Vector3()
|
||||
const _normal = new Vector3()
|
||||
const _triangle = new Triangle()
|
||||
|
||||
export default class MeshTriangulationHelper {
|
||||
/**
|
||||
* Calculates the triangulation of the face at given faceIndex.
|
||||
@@ -9,30 +19,39 @@ export default class MeshTriangulationHelper {
|
||||
* @param {Number} faceIndex The index of the face's cardinality indicator `n`
|
||||
* @param {Number[]} faces The list of faces in the mesh
|
||||
* @param {Number[]} vertices The list of vertices in the mesh
|
||||
* @return {Number[]} flat list of triangle faces (without cardinality indicators)
|
||||
* @return {Number} flat list of triangle faces (without cardinality indicators)
|
||||
*/
|
||||
static triangulateFace(faceIndex, faces, vertices) {
|
||||
let n = faces[faceIndex]
|
||||
static triangulateFace(
|
||||
faceIndex,
|
||||
faces,
|
||||
vertices,
|
||||
/** Purists rolling over in their graves because of this */
|
||||
_inout_targetArray,
|
||||
_in_offset
|
||||
) {
|
||||
let n = faces.get(faceIndex)
|
||||
if (n < 3) n += 3 // 0 -> 3, 1 -> 4
|
||||
|
||||
//Converts from relative to absolute index (returns index in mesh.vertices list)
|
||||
/** Why doesn't javascript have a means to inline functions?! */
|
||||
function asIndex(v) {
|
||||
return faceIndex + v + 1
|
||||
}
|
||||
|
||||
//Gets vertex from a relative vert index
|
||||
function V(v) {
|
||||
const index = faces[asIndex(v)] * 3
|
||||
return new Vector3(vertices[index], vertices[index + 1], vertices[index + 2])
|
||||
function V(v, target) {
|
||||
const index = faces.get(asIndex(v)) * 3
|
||||
target.x = vertices.get(index)
|
||||
target.y = vertices.get(index + 1)
|
||||
target.z = vertices.get(index + 2)
|
||||
return target
|
||||
}
|
||||
|
||||
const triangleFaces = Array((n - 2) * 3)
|
||||
|
||||
//Calculate face normal using the Newell Method
|
||||
const faceNormal = new Vector3(0, 0, 0)
|
||||
const faceNormal = _normal
|
||||
for (let ii = n - 1, jj = 0; jj < n; ii = jj, jj++) {
|
||||
const iPos = V(ii)
|
||||
const jPos = V(jj)
|
||||
const iPos = V(ii, _vec30)
|
||||
const jPos = V(jj, _vec31)
|
||||
faceNormal.x += (jPos.y - iPos.y) * (iPos.z + jPos.z) // projection on yz
|
||||
faceNormal.y += (jPos.z - iPos.z) * (iPos.x + jPos.x) // projection on xz
|
||||
faceNormal.z += (jPos.x - iPos.x) * (iPos.y + jPos.y) // projection on xy
|
||||
@@ -40,8 +59,8 @@ export default class MeshTriangulationHelper {
|
||||
faceNormal.normalize()
|
||||
|
||||
//Set up previous and next links to effectively form a double-linked vertex list
|
||||
const prev = Array(n)
|
||||
const next = Array(n)
|
||||
const prev = [] //new Array(n)
|
||||
const next = [] //new Array(n)
|
||||
for (let j = 0; j < n; j++) {
|
||||
prev[j] = j - 1
|
||||
next[j] = j + 1
|
||||
@@ -52,20 +71,25 @@ export default class MeshTriangulationHelper {
|
||||
//Start clipping ears until we are left with a triangle
|
||||
let i = 0
|
||||
let counter = 0
|
||||
let localOffset = 0
|
||||
while (n >= 3) {
|
||||
let isEar = true
|
||||
|
||||
//If we are the last triangle or we have exhausted our vertices, the below statement will be false
|
||||
if (n > 3 && counter < n) {
|
||||
const prevVertex = V(prev[i])
|
||||
const earVertex = V(i)
|
||||
const nextVertex = V(next[i])
|
||||
const prevVertex = V(prev[i], _vec30)
|
||||
const earVertex = V(i, _vec31)
|
||||
const nextVertex = V(next[i], _vec32)
|
||||
|
||||
if (this.triangleIsCCW(faceNormal, prevVertex, earVertex, nextVertex)) {
|
||||
_triangle.a.copy(prevVertex)
|
||||
_triangle.b.copy(earVertex)
|
||||
_triangle.c.copy(nextVertex)
|
||||
|
||||
if (_triangle.isFrontFacing(faceNormal)) {
|
||||
let k = next[next[i]]
|
||||
|
||||
do {
|
||||
if (this.testPointTriangle(V(k), prevVertex, earVertex, nextVertex)) {
|
||||
if (_triangle.containsPoint(V(k, _vec33))) {
|
||||
isEar = false
|
||||
break
|
||||
}
|
||||
@@ -78,10 +102,13 @@ export default class MeshTriangulationHelper {
|
||||
}
|
||||
|
||||
if (isEar) {
|
||||
const a = faces[asIndex(i)]
|
||||
const b = faces[asIndex(next[i])]
|
||||
const c = faces[asIndex(prev[i])]
|
||||
triangleFaces.push(a, b, c)
|
||||
const a = faces.get(asIndex(i))
|
||||
const b = faces.get(asIndex(next[i]))
|
||||
const c = faces.get(asIndex(prev[i]))
|
||||
_inout_targetArray[_in_offset + localOffset] = a
|
||||
_inout_targetArray[_in_offset + localOffset + 1] = b
|
||||
_inout_targetArray[_in_offset + localOffset + 2] = c
|
||||
localOffset += 3
|
||||
|
||||
next[prev[i]] = next[i]
|
||||
prev[next[i]] = prev[i]
|
||||
@@ -94,89 +121,6 @@ export default class MeshTriangulationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
return triangleFaces
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if point v is within the triangle *abc*.
|
||||
* @param {Vector3} v
|
||||
* @param {Vector3} a
|
||||
* @param {Vector3} b
|
||||
* @param {Vector3} c
|
||||
* @returns {boolean} true if v is within triangle.
|
||||
*/
|
||||
static testPointTriangle(v, a, b, c) {
|
||||
function Test(_v, _a, _b) {
|
||||
const crossA = _v.cross(_a)
|
||||
const crossB = _v.cross(_b)
|
||||
const dotWithEpsilon = Number.EPSILON + crossA.dot(crossB)
|
||||
return Math.sign(dotWithEpsilon) !== -1
|
||||
}
|
||||
|
||||
return (
|
||||
Test(b.sub(a), v.sub(a), c.sub(a)) &&
|
||||
Test(c.sub(b), v.sub(b), a.sub(b)) &&
|
||||
Test(a.sub(c), v.sub(c), b.sub(c))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that triangle abc is clockwise with reference to referenceNormal.
|
||||
* @param {Vector3} referenceNormal The normal direction of the face.
|
||||
* @param {Vector3} a
|
||||
* @param {Vector3} b
|
||||
* @param {Vector3} c
|
||||
* @returns {boolean} true if triangle is ccw
|
||||
*/
|
||||
static triangleIsCCW(referenceNormal, a, b, c) {
|
||||
const triangleNormal = c.sub(a).cross(b.sub(a))
|
||||
triangleNormal.normalize()
|
||||
return referenceNormal.dot(triangleNormal) > 0.0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates vector maths operations required for polygon triangulation
|
||||
*/
|
||||
class Vector3 {
|
||||
constructor(x, y, z) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.z = z
|
||||
}
|
||||
|
||||
add(v) {
|
||||
return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z)
|
||||
}
|
||||
|
||||
sub(v) {
|
||||
return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z)
|
||||
}
|
||||
|
||||
mul(n) {
|
||||
return new Vector3(this.x - n, this.y - n, this.z - n)
|
||||
}
|
||||
|
||||
dot(v) {
|
||||
return this.x * v.x + this.y * v.y + this.z * v.z
|
||||
}
|
||||
|
||||
cross(v) {
|
||||
const nx = this.y * v.z - this.z * v.y
|
||||
const ny = this.z * v.x - this.x * v.z
|
||||
const nz = this.x * v.y - this.y * v.x
|
||||
|
||||
return new Vector3(nx, ny, nz)
|
||||
}
|
||||
|
||||
squareSum() {
|
||||
return this.x * this.x + this.y * this.y + this.z * this.z
|
||||
}
|
||||
|
||||
normalize() {
|
||||
const scale = 1.0 / Math.sqrt(this.squareSum())
|
||||
this.x *= scale
|
||||
this.y *= scale
|
||||
this.z *= scale
|
||||
return localOffset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { TypedArray } from 'type-fest'
|
||||
import { DataChunk } from '../../IViewer.js'
|
||||
import { Box3, MathUtils, Vector3 } from 'three'
|
||||
|
||||
export class VirtualArray {
|
||||
private offsets: number[]
|
||||
|
||||
constructor(public chunks: Array<Array<number>>) {
|
||||
this.updateOffsets()
|
||||
}
|
||||
|
||||
get length() {
|
||||
if (this.chunks.length === 0) return 0
|
||||
const lastChunk = this.chunks[this.chunks.length - 1]
|
||||
return this.offsets[this.offsets.length - 1] + lastChunk.length
|
||||
}
|
||||
|
||||
get(index: number): number {
|
||||
if (this.chunks.length === 1) return this.chunks[0][index]
|
||||
const chunkIndex = this.findChunkIndex(index)
|
||||
const localIndex = index - this.offsets[chunkIndex]
|
||||
return this.chunks[chunkIndex][localIndex]
|
||||
}
|
||||
|
||||
set(index: number, value: number) {
|
||||
if (this.chunks.length === 1) {
|
||||
this.chunks[0][index] = value
|
||||
return
|
||||
}
|
||||
const chunkIndex = this.findChunkIndex(index)
|
||||
const localIndex = index - this.offsets[chunkIndex]
|
||||
this.chunks[chunkIndex][localIndex] = value
|
||||
}
|
||||
|
||||
public findChunkIndex(index: number): number {
|
||||
let low = 0
|
||||
let high = this.offsets.length - 1
|
||||
|
||||
while (low <= high) {
|
||||
const mid = (low + high) >> 1
|
||||
const start = this.offsets[mid]
|
||||
const end = mid + 1 < this.offsets.length ? this.offsets[mid + 1] : this.length
|
||||
if (index >= start && index < end) return mid
|
||||
if (index < start) high = mid - 1
|
||||
else low = mid + 1
|
||||
}
|
||||
|
||||
throw new RangeError('Index out of bounds')
|
||||
}
|
||||
|
||||
public updateOffsets() {
|
||||
this.offsets = []
|
||||
let sum = 0
|
||||
for (const chunk of this.chunks) {
|
||||
this.offsets.push(sum)
|
||||
sum += chunk.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ChunkArray extends VirtualArray {
|
||||
public chunkArray: Array<DataChunk>
|
||||
protected flatArray: TypedArray
|
||||
|
||||
constructor(chunks: Array<DataChunk>) {
|
||||
super(chunks && chunks.map((c: DataChunk) => c.data))
|
||||
this.chunkArray = chunks
|
||||
}
|
||||
|
||||
public slice() {
|
||||
const copiesArray: Array<DataChunk> = []
|
||||
this.chunkArray.forEach((chunk: DataChunk) => {
|
||||
const chunkCopy = new Array<number>(chunk.data.length)
|
||||
for (let k = 0; k < chunk.data.length; k++) {
|
||||
chunkCopy[k] = chunk.data[k]
|
||||
}
|
||||
copiesArray.push({ data: chunkCopy, id: MathUtils.generateUUID(), references: 1 })
|
||||
})
|
||||
return new ChunkArray(copiesArray)
|
||||
}
|
||||
|
||||
public copyToBuffer(buffer: TypedArray, offset: number) {
|
||||
let chunkOffset = 0
|
||||
|
||||
this.chunkArray.forEach((chunk: DataChunk) => {
|
||||
buffer.set(
|
||||
chunk.data as unknown as ArrayLike<number> & ArrayLike<bigint>,
|
||||
offset + chunkOffset
|
||||
)
|
||||
chunkOffset += chunk.data.length
|
||||
})
|
||||
}
|
||||
|
||||
public computeBox3(): Box3 {
|
||||
const box = new Box3()
|
||||
const vec3 = new Vector3()
|
||||
let carry: number[] = [] // to hold x/y if vec3 is split
|
||||
|
||||
for (let c = 0; c < this.chunks.length; c++) {
|
||||
const chunk = this.chunks[c]
|
||||
let i = 0
|
||||
|
||||
// Handle carry-over from previous chunk
|
||||
if (carry.length > 0) {
|
||||
while (carry.length < 3 && i < chunk.length) {
|
||||
carry.push(chunk[i++])
|
||||
}
|
||||
if (carry.length === 3) {
|
||||
vec3.set(carry[0], carry[1], carry[2])
|
||||
box.expandByPoint(vec3)
|
||||
carry = []
|
||||
}
|
||||
}
|
||||
|
||||
// Now read as many full vec3s as possible from this chunk
|
||||
const fullVec3Count = Math.floor((chunk.length - i) / 3)
|
||||
for (let j = 0; j < fullVec3Count; j++) {
|
||||
const x = chunk[i++]
|
||||
const y = chunk[i++]
|
||||
const z = chunk[i++]
|
||||
vec3.set(x, y, z)
|
||||
box.expandByPoint(vec3)
|
||||
}
|
||||
|
||||
// If there's a leftover partial vec3 at the end, save it
|
||||
while (i < chunk.length) {
|
||||
carry.push(chunk[i++])
|
||||
}
|
||||
}
|
||||
|
||||
// Final sanity check
|
||||
if (carry.length !== 0) {
|
||||
console.warn('Virtual position buffer ended with incomplete vec3 data')
|
||||
}
|
||||
|
||||
return box
|
||||
}
|
||||
|
||||
protected getFlatArray<T extends TypedArray>(Type: { new (length: number): T }) {
|
||||
if (!this.flatArray || !(this.flatArray instanceof Type)) {
|
||||
this.flatArray = new Type(this.length)
|
||||
let chunkOffset = 0
|
||||
this.chunks.forEach((chunk: number[]) => {
|
||||
this.flatArray.set(
|
||||
chunk as unknown as ArrayLike<number> & ArrayLike<bigint>,
|
||||
chunkOffset
|
||||
)
|
||||
chunkOffset += chunk.length
|
||||
})
|
||||
}
|
||||
return this.flatArray as T
|
||||
}
|
||||
|
||||
public getFloat32Array(): Float32Array {
|
||||
return this.getFlatArray(Float32Array)
|
||||
}
|
||||
|
||||
public getFloat64Array(): Float64Array {
|
||||
return this.getFlatArray(Float64Array)
|
||||
}
|
||||
|
||||
public getInt16Array(): Int16Array {
|
||||
return this.getFlatArray(Int16Array)
|
||||
}
|
||||
|
||||
public getInt32Array(): Int32Array {
|
||||
return this.getFlatArray(Int32Array)
|
||||
}
|
||||
|
||||
public getUint16Array(): Uint16Array {
|
||||
return this.getFlatArray(Uint16Array)
|
||||
}
|
||||
|
||||
public getUint32Array(): Uint32Array {
|
||||
return this.getFlatArray(Uint32Array)
|
||||
}
|
||||
}
|
||||
@@ -1279,8 +1279,8 @@ class TransformControlsGizmo extends Object3D {
|
||||
[0, 0, 0],
|
||||
[0, 0, -Math.PI / 2]
|
||||
]
|
||||
],
|
||||
E: [[new Mesh(new TorusGeometry(0.75, 0.1, 2, 24), matInvisible)]]
|
||||
]
|
||||
// E: [[new Mesh(new TorusGeometry(0.75, 0.1, 2, 24), matInvisible)]]
|
||||
}
|
||||
|
||||
const gizmoScale = {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Quaternion,
|
||||
Raycaster,
|
||||
ReplaceStencilOp,
|
||||
Uint16BufferAttribute,
|
||||
Vector2,
|
||||
Vector3,
|
||||
type Intersection
|
||||
@@ -370,7 +371,7 @@ export class AreaMeasurement extends Measurement {
|
||||
}
|
||||
|
||||
if (!index || index.count !== indices.length) {
|
||||
geometry.setIndex(new BufferAttribute(new Uint16Array(indices), 1))
|
||||
geometry.setIndex(new Uint16BufferAttribute(indices, 1))
|
||||
} else {
|
||||
;(index.array as Uint16Array).set(indices, 0)
|
||||
index.needsUpdate = true
|
||||
|
||||
@@ -303,6 +303,7 @@ export class SectionOutlines extends Extension {
|
||||
private createPlaneOutline(planeId: string): PlaneOutline {
|
||||
const buffer = new Float64Array(SectionOutlines.INITIAL_BUFFER_SIZE)
|
||||
const lineGeometry = new LineSegmentsGeometry()
|
||||
/** We need to re-allocate, otherwise three.js will do it anyway */
|
||||
lineGeometry.setPositions(new Float32Array(buffer))
|
||||
;(
|
||||
lineGeometry.attributes['instanceStart'] as InterleavedBufferAttribute
|
||||
@@ -390,7 +391,7 @@ export class SectionOutlines extends Extension {
|
||||
|
||||
const buffer = new Float32Array(size)
|
||||
outline.renderable.geometry = new LineSegmentsGeometry()
|
||||
outline.renderable.geometry.setPositions(new Float32Array(buffer))
|
||||
outline.renderable.geometry.setPositions(buffer)
|
||||
;(
|
||||
outline.renderable.geometry.attributes[
|
||||
'instanceStart'
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
Color,
|
||||
MeshBasicMaterial,
|
||||
PlaneGeometry,
|
||||
Float32BufferAttribute,
|
||||
Uint16BufferAttribute,
|
||||
Euler
|
||||
} from 'three'
|
||||
import { intersectObjectWithRay, TransformControls } from '../TransformControls.js'
|
||||
@@ -61,7 +63,7 @@ const _vector3 = new Vector3()
|
||||
const _tempEuler = new Euler()
|
||||
const _tempQuaternion = new Quaternion()
|
||||
|
||||
const unitCube = [
|
||||
const unitCube = new Float32Array([
|
||||
-1 * 0.5,
|
||||
-1 * 0.5,
|
||||
-1 * 0.5,
|
||||
@@ -93,12 +95,12 @@ const unitCube = [
|
||||
-1 * 0.5,
|
||||
1 * 0.5,
|
||||
1 * 0.5
|
||||
]
|
||||
])
|
||||
|
||||
const unitCubeIndices: number[] = [
|
||||
const unitCubeIndices: Uint16Array = new Uint16Array([
|
||||
0, 1, 3, 3, 1, 2, 1, 5, 2, 2, 5, 6, 5, 4, 6, 6, 4, 7, 4, 0, 7, 7, 0, 3, 3, 2, 7, 7, 2,
|
||||
6, 4, 5, 0, 0, 5, 1
|
||||
]
|
||||
])
|
||||
|
||||
const unitCubeEdges: number[] = [
|
||||
// Bottom Face
|
||||
@@ -932,11 +934,11 @@ export class SectionTool extends Extension {
|
||||
/** Creates the geometry for the visible outline of the section tool */
|
||||
protected createOutline() {
|
||||
/** We start from the unit cube's edges */
|
||||
const buffer = new Float32Array(unitCubeEdges.slice())
|
||||
const buffer = unitCubeEdges.slice() as unknown as Float32Array
|
||||
|
||||
/** Create the line segments geometry */
|
||||
const lineGeometry = new LineSegmentsGeometry()
|
||||
lineGeometry.setPositions(new Float32Array(buffer))
|
||||
lineGeometry.setPositions(buffer)
|
||||
;(
|
||||
lineGeometry.attributes['instanceStart'] as InterleavedBufferAttribute
|
||||
).data.setUsage(DynamicDrawUsage)
|
||||
@@ -1015,8 +1017,8 @@ export class SectionTool extends Extension {
|
||||
const indexes = unitCubeIndices.slice()
|
||||
|
||||
const g = new BufferGeometry()
|
||||
g.setAttribute('position', new BufferAttribute(new Float32Array(vertices), 3))
|
||||
g.setIndex(indexes)
|
||||
g.setAttribute('position', new Float32BufferAttribute(vertices, 3))
|
||||
g.setIndex(new Uint16BufferAttribute(indexes, 1))
|
||||
g.computeBoundingBox()
|
||||
g.computeVertexNormals()
|
||||
return g
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SpeckleType, type SpeckleObject } from '../../../index.js'
|
||||
import Logger from '../../utils/Logger.js'
|
||||
import { ObjectLoader2 } from '@speckle/objectloader2'
|
||||
import { SpeckleTypeAllRenderables } from '../GeometryConverter.js'
|
||||
import { DataChunk } from '../../../IViewer.js'
|
||||
|
||||
export type ConverterResultDelegate = (count: number) => void
|
||||
export type SpeckleConverterNodeDelegate =
|
||||
@@ -64,7 +65,7 @@ export default class SpeckleConverter {
|
||||
Parameter: null
|
||||
}
|
||||
|
||||
protected readonly IgnoreNodes = ['Parameter']
|
||||
protected readonly IgnoreNodes = ['Parameter', 'RawEncoding']
|
||||
|
||||
constructor(objectLoader: ObjectLoader2, tree: WorldTree) {
|
||||
if (!objectLoader) {
|
||||
@@ -287,24 +288,49 @@ export default class SpeckleConverter {
|
||||
* @param {[type]} arr [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
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
|
||||
private async dechunk(arr: Array<{ referencedId: string }>): Promise<DataChunk[]> {
|
||||
if (!arr || arr.length === 0) {
|
||||
return arr as unknown as DataChunk[]
|
||||
}
|
||||
|
||||
const chunked: unknown[] = []
|
||||
if (Array.isArray(arr[0]) && !arr[0].referencedId) {
|
||||
return arr as unknown as DataChunk[]
|
||||
}
|
||||
// Handles pre-chunking objects, or arrs that have not been chunked
|
||||
if (!arr[0].referencedId) {
|
||||
if (!(arr[0] instanceof Object))
|
||||
return [
|
||||
{
|
||||
data: arr,
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
] as unknown as DataChunk[]
|
||||
else return arr as unknown as DataChunk[]
|
||||
}
|
||||
|
||||
const chunked: DataChunk[] = []
|
||||
for (const ref of arr) {
|
||||
const real: Record<string, unknown> = (await this.objectLoader.getObject({
|
||||
const real: DataChunk = (await this.objectLoader.getObject({
|
||||
id: ref.referencedId
|
||||
})) as unknown as Record<string, number>
|
||||
chunked.push(real.data)
|
||||
})) as unknown as DataChunk
|
||||
if (real.references === undefined) {
|
||||
real.references = 1
|
||||
} else {
|
||||
real.references++
|
||||
}
|
||||
if (typeof real.data[0] !== 'number' || isNaN(real.data[0])) {
|
||||
Logger.error(
|
||||
`Chunk id ${real.id} used for mesh ${ref.referencedId} might not have numeric geometry data. This is not supported!`
|
||||
)
|
||||
}
|
||||
chunked.push(real)
|
||||
// await this.asyncPause()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const dechunked = [].concat(...(chunked as any))
|
||||
// const dechunked = [].concat(...(chunked as any))
|
||||
|
||||
return dechunked
|
||||
return chunked
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -916,14 +942,23 @@ export default class SpeckleConverter {
|
||||
Logger.warn(
|
||||
`Object id ${obj.id} of type ${obj.speckle_type} has no vertex position data and will be ignored`
|
||||
)
|
||||
node.model.raw.vertices = []
|
||||
node.model.raw.faces = []
|
||||
node.model.raw.colors = []
|
||||
node.model.raw.vertexNormals = []
|
||||
return
|
||||
}
|
||||
if (!obj.faces || (obj.faces as Array<number>).length === 0) {
|
||||
Logger.warn(
|
||||
`Object id ${obj.id} of type ${obj.speckle_type} has no face data and will be ignored`
|
||||
)
|
||||
node.model.raw.vertices = []
|
||||
node.model.raw.faces = []
|
||||
node.model.raw.colors = []
|
||||
node.model.raw.vertexNormals = []
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
node.model.raw.vertices = await this.dechunk(obj.vertices)
|
||||
|
||||
@@ -2,9 +2,11 @@ import { Geometry, type GeometryData } from '../../converter/Geometry.js'
|
||||
import MeshTriangulationHelper from '../../converter/MeshTriangulationHelper.js'
|
||||
import { getConversionFactor } from '../../converter/Units.js'
|
||||
import { type NodeData } from '../../tree/WorldTree.js'
|
||||
import { Box3, EllipseCurve, Matrix4, Vector2, Vector3 } from 'three'
|
||||
import { Box3, EllipseCurve, MathUtils, Matrix4, Vector2, Vector3 } from 'three'
|
||||
import { GeometryConverter, SpeckleType } from '../GeometryConverter.js'
|
||||
import Logger from '../../utils/Logger.js'
|
||||
import { DataChunk } from '../../../IViewer.js'
|
||||
import { ChunkArray } from '../../converter/VirtualArray.js'
|
||||
|
||||
export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
public typeLookupTable: { [type: string]: SpeckleType } = {}
|
||||
@@ -93,9 +95,35 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
node.raw.colors = []
|
||||
break
|
||||
case SpeckleType.Mesh:
|
||||
/** Raw objects will no longer hold references to chunks */
|
||||
node.raw.vertices = []
|
||||
node.raw.faces = []
|
||||
node.raw.colors = []
|
||||
node.raw.normals = []
|
||||
|
||||
// /** We can already delete these because we don't need them after triangulation */
|
||||
// node.raw.faces.forEach((c: DataChunk) => {
|
||||
// c.references--
|
||||
|
||||
// if (!c.references) {
|
||||
// Logger.warn(`Deleting chunk data ${c.id}`)
|
||||
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// //@ts-ignore
|
||||
// delete c.data
|
||||
// }
|
||||
// })
|
||||
|
||||
/** We can already delete this because we've changes the colors to floats in linear space */
|
||||
node.raw.colors.forEach((c: DataChunk) => {
|
||||
c.references--
|
||||
|
||||
if (!c.references) {
|
||||
Logger.warn(`Deleting chunk data ${c.id}`)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
delete c.data
|
||||
}
|
||||
})
|
||||
break
|
||||
case SpeckleType.Point:
|
||||
if (node.raw.value) node.raw.value = []
|
||||
@@ -197,8 +225,10 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
protected PointcloudToGeometryData(node: NodeData): GeometryData | null {
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
|
||||
const vertices = node.instanced ? node.raw.points.slice() : node.raw.points
|
||||
const colorsRaw = node.raw.colors
|
||||
const vertices = new ChunkArray(
|
||||
node.instanced ? node.raw.points.slice() : node.raw.points
|
||||
)
|
||||
const colorsRaw = new ChunkArray(node.raw.colors)
|
||||
let colors = null
|
||||
|
||||
if (colorsRaw && colorsRaw.length !== 0) {
|
||||
@@ -215,6 +245,10 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
attributes: {
|
||||
POSITION: vertices,
|
||||
COLOR: colors
|
||||
? new ChunkArray([
|
||||
{ data: colors, id: MathUtils.generateUUID(), references: 1 }
|
||||
])
|
||||
: undefined
|
||||
},
|
||||
bakeTransform: new Matrix4().makeScale(
|
||||
conversionFactor,
|
||||
@@ -254,45 +288,101 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
if (!node.raw) return null
|
||||
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
const indices = []
|
||||
|
||||
if (!node.raw.vertices) return null
|
||||
if (!node.raw.faces) return null
|
||||
|
||||
const start = performance.now()
|
||||
const vertices = node.raw.vertices
|
||||
const faces = node.raw.faces
|
||||
const colorsRaw = node.raw.colors
|
||||
|
||||
const vertices = new ChunkArray(node.raw.vertices)
|
||||
const faces = new ChunkArray(node.raw.faces)
|
||||
const colorsRaw = this.chunkArrayHasData(node.raw.colors)
|
||||
? new ChunkArray(node.raw.colors)
|
||||
: undefined
|
||||
let normals = node.raw.vertexNormals
|
||||
? new ChunkArray(node.raw.vertexNormals)
|
||||
: undefined
|
||||
let colors = undefined
|
||||
let k = 0
|
||||
let triangulated = true
|
||||
let triangulatedArraySize = 0
|
||||
while (k < faces.length) {
|
||||
let n = faces[k]
|
||||
const chunkIndex = faces.findChunkIndex(k)
|
||||
if (faces.chunkArray[chunkIndex].processed) {
|
||||
k += faces.chunkArray[chunkIndex].data.length
|
||||
continue
|
||||
}
|
||||
let n = faces.get(k)
|
||||
if (n < 3) n += 3 // 0 -> 3, 1 -> 4
|
||||
k += n + 1
|
||||
|
||||
if (n === 3) {
|
||||
const startP = performance.now()
|
||||
// Triangle face
|
||||
indices.push(faces[k + 1], faces[k + 2], faces[k + 3])
|
||||
this.pushTime += performance.now() - startP
|
||||
triangulatedArraySize += 3
|
||||
continue
|
||||
} else {
|
||||
// Quad or N-gon face
|
||||
const start1 = performance.now()
|
||||
const triangulation = MeshTriangulationHelper.triangulateFace(
|
||||
k,
|
||||
faces,
|
||||
vertices
|
||||
)
|
||||
this.actualTriangulateTime += performance.now() - start1
|
||||
indices.push(
|
||||
...triangulation.filter((el) => {
|
||||
return el !== undefined
|
||||
})
|
||||
)
|
||||
triangulatedArraySize += (n - 2) * 3
|
||||
triangulated = false
|
||||
}
|
||||
|
||||
k += n + 1
|
||||
}
|
||||
|
||||
const indices =
|
||||
triangulatedArraySize >= 65535 || vertices.length >= 65535
|
||||
? new Uint32Array(triangulatedArraySize)
|
||||
: new Uint16Array(triangulatedArraySize)
|
||||
let indicesOffset = 0
|
||||
|
||||
if (triangulated) {
|
||||
/** If already triangulated modfy the faces array in place */
|
||||
faces.chunkArray.forEach((chunk: DataChunk) => {
|
||||
if (chunk.processed) return
|
||||
|
||||
let write = 0
|
||||
for (let read = 0; read < chunk.data.length; read++) {
|
||||
if (read % 4 !== 0) {
|
||||
chunk.data[write++] = chunk.data[read]
|
||||
}
|
||||
}
|
||||
chunk.data.length = write
|
||||
chunk.processed = true
|
||||
})
|
||||
faces.updateOffsets()
|
||||
} else {
|
||||
k = 0
|
||||
while (k < faces.length) {
|
||||
/** We skip to the end of triangulated chunks */
|
||||
const chunkIndex = faces.findChunkIndex(k)
|
||||
if (faces.chunkArray[chunkIndex].processed) {
|
||||
indices.set(faces.chunks[chunkIndex], k)
|
||||
k += faces.chunkArray[chunkIndex].data.length
|
||||
continue
|
||||
}
|
||||
let n = faces.get(k)
|
||||
if (n < 3) n += 3 // 0 -> 3, 1 -> 4
|
||||
if (n === 3) {
|
||||
// Triangle face
|
||||
indices[indicesOffset] = faces.get(k + 1)
|
||||
indices[indicesOffset + 1] = faces.get(k + 2)
|
||||
indices[indicesOffset + 2] = faces.get(k + 3)
|
||||
indicesOffset += 3
|
||||
} else {
|
||||
const start1 = performance.now()
|
||||
const indexCount = MeshTriangulationHelper.triangulateFace(
|
||||
k,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
faces,
|
||||
vertices,
|
||||
indices, // inout
|
||||
indicesOffset // in
|
||||
)
|
||||
indicesOffset += indexCount
|
||||
this.actualTriangulateTime += performance.now() - start1
|
||||
}
|
||||
|
||||
k += n + 1
|
||||
}
|
||||
}
|
||||
|
||||
this.meshTriangulationTime += performance.now() - start
|
||||
|
||||
if (colorsRaw && colorsRaw.length !== 0) {
|
||||
@@ -302,7 +392,13 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
)
|
||||
} else
|
||||
/** We want the colors in linear space */
|
||||
colors = this.unpackColors(colorsRaw, true)
|
||||
colors = new ChunkArray([
|
||||
{
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1,
|
||||
data: this.unpackColors(colorsRaw, true)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
if (normals && normals.length !== 0) {
|
||||
@@ -312,12 +408,22 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
)
|
||||
normals = undefined
|
||||
}
|
||||
} else normals = undefined
|
||||
} else {
|
||||
normals = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: vertices,
|
||||
INDEX: indices,
|
||||
INDEX: triangulated
|
||||
? faces
|
||||
: new ChunkArray([
|
||||
{
|
||||
data: indices as unknown as number[],
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
]),
|
||||
...(colors && { COLOR: colors }),
|
||||
...(normals && { NORMAL: normals })
|
||||
},
|
||||
@@ -368,15 +474,18 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
*/
|
||||
protected PointToGeometryData(node: NodeData): GeometryData | null {
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
const points = this.PointToFloatArray(
|
||||
node.raw as { value: Array<number>; units: string } & {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
)
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.PointToFloatArray(
|
||||
node.raw as { value: Array<number>; units: string } & {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
)
|
||||
POSITION: new ChunkArray([
|
||||
{ data: points, id: MathUtils.generateUUID(), references: 1 }
|
||||
])
|
||||
},
|
||||
bakeTransform: new Matrix4().makeScale(
|
||||
conversionFactor,
|
||||
@@ -394,9 +503,15 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.PointToFloatArray(node.raw.start).concat(
|
||||
this.PointToFloatArray(node.raw.end)
|
||||
)
|
||||
POSITION: new ChunkArray([
|
||||
{
|
||||
data: this.PointToFloatArray(node.raw.start).concat(
|
||||
this.PointToFloatArray(node.raw.end)
|
||||
),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
bakeTransform: new Matrix4().makeScale(
|
||||
conversionFactor,
|
||||
@@ -412,12 +527,26 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
*/
|
||||
protected PolylineToGeometryData(node: NodeData): GeometryData | null {
|
||||
const conversionFactor = getConversionFactor(node.raw.units)
|
||||
const chunkArray = new ChunkArray(node.raw.value)
|
||||
|
||||
if (node.raw.closed)
|
||||
node.raw.value.push(node.raw.value[0], node.raw.value[1], node.raw.value[2])
|
||||
let outChunk = chunkArray
|
||||
if (node.raw.closed) {
|
||||
const complete = new Float32Array(chunkArray.length + 3)
|
||||
chunkArray.copyToBuffer(complete, 0)
|
||||
complete[chunkArray.length] = complete[0]
|
||||
complete[chunkArray.length + 1] = complete[1]
|
||||
complete[chunkArray.length + 2] = complete[2]
|
||||
outChunk = new ChunkArray([
|
||||
{
|
||||
data: complete as unknown as number[],
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
}
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: node.raw.value.slice(0)
|
||||
POSITION: outChunk
|
||||
},
|
||||
bakeTransform: new Matrix4().makeScale(
|
||||
conversionFactor,
|
||||
@@ -504,7 +633,9 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: edges
|
||||
POSITION: new ChunkArray([
|
||||
{ data: edges, id: MathUtils.generateUUID(), references: 1 }
|
||||
])
|
||||
},
|
||||
bakeTransform: new Matrix4().copy(T).multiply(R),
|
||||
transform: null
|
||||
@@ -562,7 +693,13 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
)
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.FlattenVector3Array(points)
|
||||
POSITION: new ChunkArray([
|
||||
{
|
||||
data: this.FlattenVector3Array(points),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
bakeTransform: null,
|
||||
transform: null
|
||||
@@ -664,7 +801,13 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.FlattenVector3Array(points)
|
||||
POSITION: new ChunkArray([
|
||||
{
|
||||
data: this.FlattenVector3Array(points),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
bakeTransform: matrix,
|
||||
transform: null
|
||||
@@ -710,7 +853,13 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
|
||||
return {
|
||||
attributes: {
|
||||
POSITION: this.FlattenVector3Array(points)
|
||||
POSITION: new ChunkArray([
|
||||
{
|
||||
data: this.FlattenVector3Array(points),
|
||||
id: MathUtils.generateUUID(),
|
||||
references: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
bakeTransform: null,
|
||||
transform: null
|
||||
@@ -818,10 +967,10 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
return output
|
||||
}
|
||||
|
||||
protected unpackColors(int32Colors: number[], tolinear = false): number[] {
|
||||
protected unpackColors(int32Colors: ChunkArray, tolinear = false): number[] {
|
||||
const colors = new Array<number>(int32Colors.length * 3)
|
||||
for (let i = 0; i < int32Colors.length; i++) {
|
||||
const color = int32Colors[i]
|
||||
const color = int32Colors.get(i)
|
||||
const r = (color >> 16) & 0xff
|
||||
const g = (color >> 8) & 0xff
|
||||
const b = color & 0xff
|
||||
@@ -843,4 +992,9 @@ export class SpeckleGeometryConverter extends GeometryConverter {
|
||||
else if (x < 0.04045) return x / 12.92
|
||||
else return Math.pow((x + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
|
||||
/** Connectors send empty chunks ಠ_ಠ */
|
||||
protected chunkArrayHasData(chunks: Array<DataChunk>): boolean {
|
||||
return chunks && chunks.filter((c: DataChunk) => c.data && c.data.length).length > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SpeckleMaterial } from './SpeckleMaterial.js'
|
||||
import SpecklePointColouredMaterial from './SpecklePointColouredMaterial.js'
|
||||
import { type Asset, AssetType, type MaterialOptions } from '../../IViewer.js'
|
||||
import SpeckleTextColoredMaterial from './SpeckleTextColoredMaterial.js'
|
||||
import { ChunkArray } from '../converter/VirtualArray.js'
|
||||
|
||||
const defaultGradient: Asset = {
|
||||
id: 'defaultGradient',
|
||||
@@ -124,6 +125,10 @@ export default class Materials {
|
||||
if (!materialNode) return null
|
||||
let renderMaterial: RenderMaterial | null = null
|
||||
if (materialNode.model.raw.renderMaterial) {
|
||||
const colorsChunkArray = geometryNode?.model.raw.colors
|
||||
? new ChunkArray(geometryNode?.model.raw.colors)
|
||||
: undefined
|
||||
|
||||
renderMaterial = {
|
||||
id: materialNode.model.raw.renderMaterial.id,
|
||||
color: materialNode.model.raw.renderMaterial.diffuse,
|
||||
@@ -135,9 +140,7 @@ export default class Materials {
|
||||
roughness: materialNode.model.raw.renderMaterial.roughness,
|
||||
metalness: materialNode.model.raw.renderMaterial.metalness,
|
||||
vertexColors:
|
||||
geometryNode &&
|
||||
geometryNode.model.raw.colors &&
|
||||
geometryNode.model.raw.colors.length > 0
|
||||
(geometryNode && colorsChunkArray && colorsChunkArray.length > 0) ?? false
|
||||
}
|
||||
}
|
||||
return renderMaterial
|
||||
@@ -273,7 +276,8 @@ export default class Materials {
|
||||
colorMaterialData &&
|
||||
!materialData &&
|
||||
(renderView.geometryType === GeometryType.LINE ||
|
||||
renderView.geometryType === GeometryType.POINT)
|
||||
renderView.geometryType === GeometryType.POINT ||
|
||||
renderView.geometryType === GeometryType.TEXT)
|
||||
|
||||
const displayStyleFirst = renderView.geometryType === GeometryType.LINE
|
||||
if (!materialData) {
|
||||
|
||||
@@ -149,7 +149,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinbase_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
|
||||
@@ -205,7 +205,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#endif
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#include <normal_vertex>
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
|
||||
@@ -146,7 +146,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#endif
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#include <normal_vertex>
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
|
||||
@@ -143,7 +143,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinbase_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
|
||||
@@ -140,7 +140,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinbase_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
|
||||
@@ -139,7 +139,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinbase_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#include <normal_vertex>
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
|
||||
@@ -158,7 +158,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinbase_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#include <normal_vertex>
|
||||
|
||||
#include <begin_vertex>
|
||||
|
||||
@@ -155,7 +155,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinbase_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#include <normal_vertex>
|
||||
|
||||
#include <begin_vertex>
|
||||
|
||||
@@ -63,7 +63,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinbase_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
|
||||
@@ -153,7 +153,32 @@ void main() {
|
||||
#include <morphnormal_vertex>
|
||||
#include <skinbase_vertex>
|
||||
#include <skinnormal_vertex>
|
||||
#include <defaultnormal_vertex>
|
||||
// #include <defaultnormal_vertex> // COMMENTED CHUNK
|
||||
vec3 transformedNormal = objectNormal;
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
// this is in lieu of a per-instance normal-matrix
|
||||
// shear transforms in the instance matrix are not supported
|
||||
mat3 m = mat3( instanceMatrix );
|
||||
transformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );
|
||||
transformedNormal = m * transformedNormal;
|
||||
|
||||
/* If we have negative scaling, we flip the normal */
|
||||
float signDet = sign(dot(m[0], cross(m[1], m[2])));
|
||||
// Optional fallback: treat 0 as +1
|
||||
signDet = signDet + (1.0 - abs(signDet));
|
||||
transformedNormal *= signDet;
|
||||
#endif
|
||||
transformedNormal = normalMatrix * transformedNormal;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedNormal = - transformedNormal;
|
||||
#endif
|
||||
#ifdef USE_TANGENT
|
||||
vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;
|
||||
#ifdef FLIP_SIDED
|
||||
transformedTangent = - transformedTangent;
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
|
||||
@@ -82,8 +82,8 @@ export class AccelerationStructure {
|
||||
}
|
||||
|
||||
public static buildBVH(
|
||||
indices: number[] | undefined,
|
||||
position: number[] | undefined,
|
||||
indices: Uint16Array | Uint32Array | undefined,
|
||||
position: Float32Array | Float64Array | undefined,
|
||||
options: BVHOptions = DefaultBVHOptions,
|
||||
transform?: Matrix4
|
||||
): MeshBVH {
|
||||
@@ -92,7 +92,7 @@ export class AccelerationStructure {
|
||||
throw new Error('Cannot build BVH with undefined indices or position!')
|
||||
}
|
||||
|
||||
let bvhPositions = new Float32Array(position)
|
||||
let bvhPositions = position
|
||||
if (transform) {
|
||||
bvhPositions = new Float32Array(position.length)
|
||||
const vecBuff = new Vector3()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Box3,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
Color,
|
||||
DoubleSide,
|
||||
@@ -24,6 +23,7 @@ import SpeckleBasicMaterial, {
|
||||
import SpeckleTextMaterial from '../materials/SpeckleTextMaterial.js'
|
||||
import { ObjectLayers } from '../../index.js'
|
||||
import Logger from '../utils/Logger.js'
|
||||
import { Uint16BufferAttribute } from 'three'
|
||||
|
||||
const _mat40: Matrix4 = new Matrix4()
|
||||
const _mat41: Matrix4 = new Matrix4()
|
||||
@@ -517,12 +517,9 @@ export class TextLabel extends Text {
|
||||
}
|
||||
|
||||
const geometry = new BufferGeometry()
|
||||
geometry.setIndex(new BufferAttribute(new Uint32Array(indices), 1))
|
||||
geometry.setAttribute(
|
||||
'position',
|
||||
new BufferAttribute(new Float32Array(positions), 3)
|
||||
)
|
||||
geometry.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2))
|
||||
geometry.setIndex(new Uint16BufferAttribute(indices, 1))
|
||||
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
|
||||
geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2))
|
||||
geometry.computeBoundingBox()
|
||||
|
||||
return geometry
|
||||
|
||||
@@ -67,24 +67,29 @@ export class TopLevelAccelerationStructure {
|
||||
}
|
||||
|
||||
private buildBVH() {
|
||||
const indices = []
|
||||
const vertices: number[] = new Array<number>(
|
||||
const indices: Uint16Array = new Uint16Array(
|
||||
TopLevelAccelerationStructure.cubeIndices.length * this.batchObjects.length
|
||||
)
|
||||
const vertices: Float32Array = new Float32Array(
|
||||
TopLevelAccelerationStructure.CUBE_VERTS * 3 * this.batchObjects.length
|
||||
)
|
||||
let vertOffset = 0
|
||||
let indexOffset = 0
|
||||
for (let k = 0; k < this.batchObjects.length; k++) {
|
||||
const boxBounds: Box3 = this.batchObjects[k].accelerationStructure.getBoundingBox(
|
||||
new Box3()
|
||||
)
|
||||
this.updateVertArray(boxBounds, vertOffset, vertices)
|
||||
indices.push(
|
||||
...TopLevelAccelerationStructure.cubeIndices.map((val) => val + vertOffset / 3)
|
||||
indices.set(
|
||||
TopLevelAccelerationStructure.cubeIndices.map((val) => val + vertOffset / 3),
|
||||
indexOffset
|
||||
)
|
||||
this.batchObjects[k].tasVertIndexStart = vertOffset / 3
|
||||
this.batchObjects[k].tasVertIndexEnd =
|
||||
vertOffset / 3 + TopLevelAccelerationStructure.CUBE_VERTS
|
||||
|
||||
vertOffset += TopLevelAccelerationStructure.CUBE_VERTS * 3
|
||||
indexOffset += TopLevelAccelerationStructure.cubeIndices.length
|
||||
}
|
||||
this.accelerationStructure = new AccelerationStructure(
|
||||
AccelerationStructure.buildBVH(indices, vertices)
|
||||
@@ -105,7 +110,7 @@ export class TopLevelAccelerationStructure {
|
||||
}
|
||||
}
|
||||
|
||||
private updateVertArray(box: Box3, offset: number, outPositions: number[]) {
|
||||
private updateVertArray(box: Box3, offset: number, outPositions: Float32Array) {
|
||||
outPositions[offset] = box.min.x
|
||||
outPositions[offset + 1] = box.min.y
|
||||
outPositions[offset + 2] = box.max.z
|
||||
@@ -141,7 +146,7 @@ export class TopLevelAccelerationStructure {
|
||||
|
||||
public refit() {
|
||||
const positions = this.accelerationStructure.geometry.attributes.position
|
||||
.array as number[]
|
||||
.array as Float32Array
|
||||
// const boxBuffer: Box3 = new Box3()
|
||||
for (let k = 0; k < this.batchObjects.length; k++) {
|
||||
const start = this.batchObjects[k].tasVertIndexStart
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box3 } from 'three'
|
||||
import { Box3, Matrix4 } from 'three'
|
||||
import { GeometryType } from '../batching/Batch.js'
|
||||
import { GeometryAttributes, type GeometryData } from '../converter/Geometry.js'
|
||||
import Materials, {
|
||||
@@ -7,6 +7,7 @@ import Materials, {
|
||||
type RenderMaterial
|
||||
} from '../materials/Materials.js'
|
||||
import { SpeckleType } from '../loaders/GeometryConverter.js'
|
||||
import { ChunkArray } from '../converter/VirtualArray.js'
|
||||
|
||||
export interface NodeRenderData {
|
||||
id: string
|
||||
@@ -84,6 +85,10 @@ export class NodeRenderView {
|
||||
return this._aabb
|
||||
}
|
||||
|
||||
public set aabb(value: Box3) {
|
||||
this._aabb.copy(value)
|
||||
}
|
||||
|
||||
public get transparent(): boolean {
|
||||
return (
|
||||
(this._renderData.renderMaterial &&
|
||||
@@ -150,14 +155,18 @@ export class NodeRenderView {
|
||||
if (vertEnd !== undefined) this._batchVertexEnd = vertEnd
|
||||
}
|
||||
|
||||
public computeAABB() {
|
||||
public computeAABB(transform?: Matrix4) {
|
||||
if (!this._aabb) this._aabb = new Box3()
|
||||
|
||||
if (
|
||||
this._renderData.geometry.attributes &&
|
||||
this._renderData.geometry.attributes.POSITION.length
|
||||
) {
|
||||
this._aabb.setFromArray(this._renderData.geometry.attributes.POSITION)
|
||||
/** For transformations that contain non-uniform scaling combine with rotation the resulting
|
||||
* aabb is not going to be accurate. We will re-compute and assign it when we build the batches
|
||||
*/
|
||||
this._aabb.copy(this._renderData.geometry.attributes.POSITION.computeBox3())
|
||||
if (transform) this._aabb.applyMatrix4(transform)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +190,9 @@ export class NodeRenderView {
|
||||
|
||||
public disposeGeometry() {
|
||||
for (const attr in this._renderData.geometry.attributes) {
|
||||
this._renderData.geometry.attributes[attr as GeometryAttributes] = []
|
||||
this._renderData.geometry.attributes[attr as GeometryAttributes] = new ChunkArray(
|
||||
[]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { type TreeNode, WorldTree } from './WorldTree.js'
|
||||
import Materials from '../materials/Materials.js'
|
||||
import { type NodeRenderData, NodeRenderView } from './NodeRenderView.js'
|
||||
import { GeometryConverter, SpeckleType } from '../loaders/GeometryConverter.js'
|
||||
import { Geometry } from '../converter/Geometry.js'
|
||||
import Logger from '../utils/Logger.js'
|
||||
|
||||
export class RenderTree {
|
||||
@@ -51,25 +50,29 @@ export class RenderTree {
|
||||
private applyTransforms(node: TreeNode) {
|
||||
if (node.model.renderView) {
|
||||
const transform = this.computeTransform(node)
|
||||
if (node.model.renderView.hasGeometry) {
|
||||
if (node.model.renderView.hasGeometry || node.model.renderView.hasMetadata) {
|
||||
if (node.model.renderView.renderData.geometry.bakeTransform) {
|
||||
transform.multiply(node.model.renderView.renderData.geometry.bakeTransform)
|
||||
}
|
||||
if (
|
||||
node.model.instanced &&
|
||||
node.model.renderView.speckleType === SpeckleType.Mesh
|
||||
)
|
||||
node.model.renderView.renderData.geometry.transform = transform
|
||||
else {
|
||||
Geometry.transformGeometryData(
|
||||
node.model.renderView.renderData.geometry,
|
||||
transform
|
||||
)
|
||||
}
|
||||
node.model.renderView.computeAABB()
|
||||
} else if (node.model.renderView.hasMetadata) {
|
||||
node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform)
|
||||
node.model.renderView.computeAABB()
|
||||
node.model.renderView.renderData.geometry.transform = transform
|
||||
node.model.renderView.computeAABB(!node.model.instanced ? transform : undefined)
|
||||
/** I like that this is gone now! */
|
||||
// if (
|
||||
// node.model.instanced &&
|
||||
// node.model.renderView.speckleType === SpeckleType.Mesh
|
||||
// )
|
||||
// node.model.renderView.renderData.geometry.transform = transform
|
||||
// else {
|
||||
// Geometry.transformGeometryData(
|
||||
// node.model.renderView.renderData.geometry,
|
||||
// transform
|
||||
// )
|
||||
// }
|
||||
// node.model.renderView.computeAABB()
|
||||
// } else if (node.model.renderView.hasMetadata) {
|
||||
// node.model.renderView.renderData.geometry.bakeTransform.premultiply(transform)
|
||||
// node.model.renderView.computeAABB()
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1162,6 +1162,12 @@ Generate the environment variables for Speckle server and Speckle objects deploy
|
||||
- name: FF_NEXT_GEN_FILE_IMPORTER_ENABLED
|
||||
value: {{ .Values.featureFlags.nextGenFileImporterEnabled | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.featureFlags.rhinoFileImporterEnabled }}
|
||||
- name: FF_RHINO_FILE_IMPORTER_ENABLED
|
||||
value: {{ .Values.featureFlags.rhinoFileImporterEnabled | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.featureFlags.backgroundJobsEnabled }}
|
||||
- name: FILEIMPORT_QUEUE_POSTGRES_URL
|
||||
valueFrom:
|
||||
|
||||
@@ -134,6 +134,11 @@
|
||||
"type": "boolean",
|
||||
"description": "Enables the ability to import data from ACC",
|
||||
"default": false
|
||||
},
|
||||
"rhinoFileImporterEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enables the dedicated Rhino based file importer. This is not part of the deployment.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -75,6 +75,8 @@ featureFlags:
|
||||
backgroundJobsEnabled: false
|
||||
## @param featureFlags.accIntegrationEnabled Enables the ability to import data from ACC
|
||||
accIntegrationEnabled: false
|
||||
## @param featureFlags.rhinoFileImporterEnabled Enables the dedicated Rhino based file importer. This is not part of the deployment.
|
||||
rhinoFileImporterEnabled: false
|
||||
|
||||
analytics:
|
||||
## @param analytics.enabled Enable or disable analytics
|
||||
|
||||
Reference in New Issue
Block a user