diff --git a/jest.config.ts b/jest.config.ts index 3debdca9..343e3a26 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -23,7 +23,10 @@ const config: Config = { transformIgnorePatterns: [ '/node_modules/(?!.*mtext-parser)' ], - testPathIgnorePatterns: ['packages/dxf-json/', '/e2e/'], + testPathIgnorePatterns: [ + '/e2e/', + '/__tests__/helpers/' + ], moduleNameMapper: { '^lodash-es$': 'lodash', '^three/examples/jsm/lines/LineMaterial\\.js$': diff --git a/packages/cad-simple-viewer/__tests__/AcTrLayout.spec.ts b/packages/cad-simple-viewer/__tests__/AcTrLayout.spec.ts index b0f940ca..8662efbd 100644 --- a/packages/cad-simple-viewer/__tests__/AcTrLayout.spec.ts +++ b/packages/cad-simple-viewer/__tests__/AcTrLayout.spec.ts @@ -104,14 +104,14 @@ function createLayerInfo(name = '0') { function createEntity( objectId: string, layerName = '0' -): AcTrEntity & { box: THREE.Box3 } { +): AcTrEntity & { wcsBbox: THREE.Box3 } { return { objectId, layerName, ownerId: 'layout-1', userData: {}, - box: new THREE.Box3(new THREE.Vector3(1, 1, 0), new THREE.Vector3(2, 2, 0)) - } as AcTrEntity & { box: THREE.Box3 } + wcsBbox: new THREE.Box3(new THREE.Vector3(1, 1, 0), new THREE.Vector3(2, 2, 0)) + } as AcTrEntity & { wcsBbox: THREE.Box3 } } describe('AcTrLayout bounding box', () => { diff --git a/packages/cad-simple-viewer/__tests__/AcTrView2d.spec.ts b/packages/cad-simple-viewer/__tests__/AcTrView2d.spec.ts new file mode 100644 index 00000000..c421dd6a --- /dev/null +++ b/packages/cad-simple-viewer/__tests__/AcTrView2d.spec.ts @@ -0,0 +1,173 @@ +jest.mock('rbush', () => { + class RBushMock< + T extends { minX: number; minY: number; maxX: number; maxY: number } + > { + private items: T[] = [] + + insert(item: T) { + this.items.push(item) + } + + load(items: readonly T[]) { + this.items.push(...items) + } + + remove(item: T, equals?: (a: T, b: T) => boolean) { + if (equals) { + const index = this.items.findIndex(entry => equals(entry, item)) + if (index >= 0) this.items.splice(index, 1) + return + } + const index = this.items.indexOf(item) + if (index >= 0) this.items.splice(index, 1) + } + + clear() { + this.items = [] + } + + search(bbox: { minX: number; minY: number; maxX: number; maxY: number }) { + return this.items.filter( + item => + !( + item.maxX < bbox.minX || + item.minX > bbox.maxX || + item.maxY < bbox.minY || + item.minY > bbox.maxY + ) + ) + } + + collides(bbox: { minX: number; minY: number; maxX: number; maxY: number }) { + return this.search(bbox).length > 0 + } + + all() { + return [...this.items] + } + } + + return { + __esModule: true, + default: RBushMock + } +}) + +import { AcCmColor, AcGeBox2d } from '@mlightcad/data-model' +import type { AcTrEntity } from '@mlightcad/three-renderer' +import * as THREE from 'three' + +import { + assertGroupWcsBboxesConsistent, + unionGroupWcsChildBoxes +} from '../src/view/AcTrGroupWcsBboxAssert' +import { AcTrLayout } from '../src/view/AcTrLayout' + +function createLayerInfo(name: string) { + return { + name, + isFrozen: false, + isOff: false, + color: new AcCmColor() + } +} + +function createHandleGroupEntity( + objectId: string, + layerName: string, + wcsBbox: THREE.Box3, + spatialIndexChildBoxes: Array<{ + minX: number + minY: number + maxX: number + maxY: number + id: string + }> +): AcTrEntity { + return { + objectId, + ownerId: 'layout-1', + layerName, + userData: { spatialIndexChildBoxes }, + wcsBbox + } as unknown as AcTrEntity +} + +describe('AcTrGroupWcsBboxAssert', () => { + it('passes when wcsBbox equals the union of wcsChildBoxes', () => { + const group = { + wcsBbox: new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(10, 15, 0)), + wcsChildBoxes: [ + { minX: 0, minY: 0, maxX: 10, maxY: 0, id: 'line-a' }, + { minX: 2, minY: 5, maxX: 8, maxY: 15, id: 'line-b' } + ] + } + + expect(() => assertGroupWcsBboxesConsistent(group)).not.toThrow() + expect(unionGroupWcsChildBoxes(group).min).toMatchObject({ x: 0, y: 0 }) + }) + + it('throws when wcsBbox diverges from wcsChildBoxes', () => { + const group = { + wcsBbox: new THREE.Box3(new THREE.Vector3(999, 999, 0), new THREE.Vector3(10, 5, 0)), + wcsChildBoxes: [{ minX: 0, minY: 0, maxX: 10, maxY: 5, id: 'line-a' }] + } + + expect(() => assertGroupWcsBboxesConsistent(group)).toThrow( + /does not match wcsChildBoxes union/ + ) + }) +}) + +describe('handleGroup WCS spatial index registration', () => { + it('indexes pre-transformed WCS bounds without applying group.matrix again', () => { + const insertWcsBbox = new THREE.Box3( + new THREE.Vector3(100, 200, 0), + new THREE.Vector3(115, 210, 0) + ) + const spatialIndexChildBoxes = [ + { minX: 100, minY: 200, maxX: 110, maxY: 200, id: 'line-0' }, + { minX: 105, minY: 205, maxX: 115, maxY: 210, id: 'line-l2' } + ] + + assertGroupWcsBboxesConsistent({ + wcsBbox: insertWcsBbox, + wcsChildBoxes: spatialIndexChildBoxes + }) + + const entity = createHandleGroupEntity( + 'INSERT-1', + 'L2', + insertWcsBbox, + spatialIndexChildBoxes + ) + + const layout = new AcTrLayout() + layout.addLayer(createLayerInfo('L2')) + layout.addEntity(entity) + + const queryBox = new AcGeBox2d() + queryBox.min.set(109, 204) + queryBox.max.set(111, 206) + + const hits = layout.search(queryBox) + + expect(hits).toHaveLength(1) + expect(hits[0].id).toBe('INSERT-1') + expect(hits[0].children?.map(item => item.id)).toEqual(['line-l2']) + }) + + it('documents that re-applying group.matrix would double-transform WCS bounds', () => { + const wcsBbox = new THREE.Box3( + new THREE.Vector3(100, 200, 0), + new THREE.Vector3(110, 205, 0) + ) + const insertMatrix = new THREE.Matrix4().makeTranslation(100, 200, 0) + + const wronglyTransformed = wcsBbox.clone() + wronglyTransformed.applyMatrix4(insertMatrix) + + expect(wronglyTransformed.min.x).toBeCloseTo(200) + expect(wcsBbox.min.x).toBeCloseTo(100) + }) +}) diff --git a/packages/cad-simple-viewer/src/spatialIndex/AcTrHierarchicalSpatialIndex.ts b/packages/cad-simple-viewer/src/spatialIndex/AcTrHierarchicalSpatialIndex.ts index e306a62d..fee8de08 100644 --- a/packages/cad-simple-viewer/src/spatialIndex/AcTrHierarchicalSpatialIndex.ts +++ b/packages/cad-simple-viewer/src/spatialIndex/AcTrHierarchicalSpatialIndex.ts @@ -266,13 +266,13 @@ export class AcTrHierarchicalSpatialIndex implements AcTrSpatialIndex { * Creates or retrieves the child index for a group object. * * This is a convenience wrapper around `ensureChildIndex`, using the group's - * `objectId` and `boxes` as id and initialization data. + * `objectId` and {@link AcTrGroup.wcsChildBoxes} as id and initialization data. * * @param group Group providing id and child box items. * @returns Existing or newly created child index, or `undefined` when empty. */ createChildIndex(group: AcTrGroup) { - return this.ensureChildIndex(group.objectId, group.boxes) + return this.ensureChildIndex(group.objectId, group.wcsChildBoxes) } /** diff --git a/packages/cad-simple-viewer/src/view/AcTrGroupWcsBboxAssert.ts b/packages/cad-simple-viewer/src/view/AcTrGroupWcsBboxAssert.ts new file mode 100644 index 00000000..2c8c917b --- /dev/null +++ b/packages/cad-simple-viewer/src/view/AcTrGroupWcsBboxAssert.ts @@ -0,0 +1,67 @@ +import type { AcTrGroup } from '@mlightcad/three-renderer' +import * as THREE from 'three' + +const WCS_BBOX_TOLERANCE = 1e-5 + +export interface AcTrGroupWcsBboxLike { + wcsBbox: THREE.Box3 + wcsChildBoxes: ReadonlyArray<{ + minX: number + minY: number + maxX: number + maxY: number + }> +} + +/** + * Unions per-child WCS boxes into one axis-aligned WCS box. + */ +export function unionGroupWcsChildBoxes(group: AcTrGroupWcsBboxLike): THREE.Box3 { + const union = new THREE.Box3() + for (const box of group.wcsChildBoxes) { + union.union( + new THREE.Box3( + new THREE.Vector3(box.minX, box.minY, 0), + new THREE.Vector3(box.maxX, box.maxY, 0) + ) + ) + } + return union +} + +/** + * Verifies that a block group's aggregate {@link AcTrGroup.wcsBbox} matches the + * union of its per-child {@link AcTrGroup.wcsChildBoxes}. + * + * Called from {@link AcTrView2d.handleGroup} in non-production builds. After + * {@link AcDbRenderingCache.draw} applies the INSERT transform via + * `applyMatrix`, both fields must stay in sync for spatial indexing. + */ +export function assertGroupWcsBboxesConsistent(group: AcTrGroupWcsBboxLike): void { + if (group.wcsChildBoxes.length === 0) { + return + } + + const union = unionGroupWcsChildBoxes(group) + const wcs = group.wcsBbox + const cornersMatch = + Math.abs(wcs.min.x - union.min.x) <= WCS_BBOX_TOLERANCE && + Math.abs(wcs.min.y - union.min.y) <= WCS_BBOX_TOLERANCE && + Math.abs(wcs.min.z - union.min.z) <= WCS_BBOX_TOLERANCE && + Math.abs(wcs.max.x - union.max.x) <= WCS_BBOX_TOLERANCE && + Math.abs(wcs.max.y - union.max.y) <= WCS_BBOX_TOLERANCE && + Math.abs(wcs.max.z - union.max.z) <= WCS_BBOX_TOLERANCE + + if (!cornersMatch) { + throw new Error( + `[AcTrView2d] Group wcsBbox [${wcs.min.x}, ${wcs.min.y}]–[${wcs.max.x}, ${wcs.max.y}] ` + + `does not match wcsChildBoxes union [${union.min.x}, ${union.min.y}]–[${union.max.x}, ${union.max.y}]. ` + + 'Ensure AcDbRenderingCache.draw applied the INSERT transform before handleGroup.' + ) + } +} + +/** @internal Narrow helper for handleGroup dev checks. */ +export function assertAcTrGroupWcsBboxesConsistent(group: AcTrGroup): void { + assertGroupWcsBboxesConsistent(group) +} diff --git a/packages/cad-simple-viewer/src/view/AcTrLayout.ts b/packages/cad-simple-viewer/src/view/AcTrLayout.ts index 8d27c991..be588519 100644 --- a/packages/cad-simple-viewer/src/view/AcTrLayout.ts +++ b/packages/cad-simple-viewer/src/view/AcTrLayout.ts @@ -119,7 +119,7 @@ export class AcTrLayout { * Gets the bounding box that contains all entities in this layout. * * Derived from packed batch vertex buffers (same source as GPU draw data), - * not from accumulated {@link AcTrEntity.box} metadata. + * not from accumulated {@link AcTrEntity.wcsBbox} metadata. * * @returns The layout's bounding box */ @@ -587,7 +587,7 @@ export class AcTrLayout { } private registerEntitySpatialIndex(entity: AcTrEntity) { - const box = entity.box + const box = entity.wcsBbox this._spatialIndex.insert({ minX: box.min.x, minY: box.min.y, diff --git a/packages/cad-simple-viewer/src/view/AcTrView2d.ts b/packages/cad-simple-viewer/src/view/AcTrView2d.ts index b4bef22a..8af8d07c 100644 --- a/packages/cad-simple-viewer/src/view/AcTrView2d.ts +++ b/packages/cad-simple-viewer/src/view/AcTrView2d.ts @@ -56,6 +56,7 @@ import { } from '../editor/global/AcEdUiColor' import { AcTrGeometryUtil } from '../util' import { AcTrEntityDisplayController } from './AcTrEntityDisplayController' +import { assertAcTrGroupWcsBboxesConsistent } from './AcTrGroupWcsBboxAssert' import { AcTrLayer } from './AcTrLayer' import { AcTrLayoutView } from './AcTrLayoutView' import { AcTrLayoutViewManager } from './AcTrLayoutViewManager' @@ -1965,37 +1966,22 @@ export class AcTrView2d extends AcEdBaseView { const renderContext = group.renderContext const groupObjectId = group.objectId const groupLayerName = group.layerName - const worldGroupBox = group.box.clone() - worldGroupBox.applyMatrix4(group.matrix) - const groupChildBoxes: AcEdSpatialQueryResultItem[] = group.boxes.map( - box => { - const points = [ - new THREE.Vector3(box.minX, box.minY, 0), - new THREE.Vector3(box.maxX, box.minY, 0), - new THREE.Vector3(box.maxX, box.maxY, 0), - new THREE.Vector3(box.minX, box.maxY, 0) - ] - for (const point of points) { - point.applyMatrix4(group.matrix) - } - let minX = Infinity - let minY = Infinity - let maxX = -Infinity - let maxY = -Infinity - for (const point of points) { - minX = Math.min(minX, point.x) - minY = Math.min(minY, point.y) - maxX = Math.max(maxX, point.x) - maxY = Math.max(maxY, point.y) - } - return { - minX, - minY, - maxX, - maxY, - id: box.id - } - } + + // AcDbRenderingCache.draw (and similar paths such as AcDbTable) already call + // applyMatrix on the group, which updates wcsBbbox and wcsChildBoxes to WCS. + // Do not multiply group.matrix here — that would double-transform spatial bounds. + if (process.env.NODE_ENV !== 'production') { + assertAcTrGroupWcsBboxesConsistent(group) + } + + const groupChildBoxes: AcEdSpatialQueryResultItem[] = group.wcsChildBoxes.map( + box => ({ + minX: box.minX, + minY: box.minY, + maxX: box.maxX, + maxY: box.maxY, + id: box.id + }) ) objectsGroupByLayer.forEach((objects, layerName) => { // AutoCAD block rule: entities authored on layer "0" inherit the INSERT's layer. @@ -2018,7 +2004,7 @@ export class AcTrView2d extends AcEdBaseView { // If block-definition entities are on layer "0", this bucket now uses the layer // of the block reference itself (effectiveLayerName). entity.layerName = effectiveLayerName - entity.box = worldGroupBox + entity.wcsBbox = group.wcsBbox.clone() const entityUserData = entity.userData as { spatialIndexChildBoxes?: AcEdSpatialQueryResultItem[] } diff --git a/packages/three-renderer/__tests__/AcTrEntity.spec.ts b/packages/three-renderer/__tests__/AcTrEntity.spec.ts new file mode 100644 index 00000000..a39cb504 --- /dev/null +++ b/packages/three-renderer/__tests__/AcTrEntity.spec.ts @@ -0,0 +1,39 @@ +import { AcGeMatrix3d } from '@mlightcad/data-model' +import * as THREE from 'three' + +import { expectWcsBboxCloseTo } from './helpers/expectWcsBbox' +import { AcTrEntity } from '../src/object/AcTrEntity' +import { AcTrRenderContext } from '../src/renderer/AcTrRenderContext' +import { AcTrStyleManager } from '../src/style/AcTrStyleManager' + +describe('AcTrEntity wcsBbox', () => { + it('starts empty and accepts explicit WCS bounds', () => { + const entity = new AcTrEntity(new AcTrRenderContext()) + + expect(entity.wcsBbox.isEmpty()).toBe(true) + + entity.wcsBbox.set(new THREE.Vector3(1, 2, 3), new THREE.Vector3(4, 5, 6)) + + expectWcsBboxCloseTo(entity.wcsBbox, [1, 2, 3], [4, 5, 6]) + }) + + it('updates wcsBbox when applyMatrix is called', () => { + const entity = new AcTrEntity(new AcTrRenderContext()) + entity.wcsBbox.set(new THREE.Vector3(0, 0, 0), new THREE.Vector3(10, 5, 0)) + + entity.applyMatrix(new AcGeMatrix3d().makeTranslation(100, 200, 0)) + + expectWcsBboxCloseTo(entity.wcsBbox, [100, 200, 0], [110, 205, 0]) + }) + + it('copies wcsBbox in fastDeepClone', () => { + const context = new AcTrRenderContext(new AcTrStyleManager()) + const entity = new AcTrEntity(context) + entity.objectId = 'entity-1' + entity.wcsBbox.set(new THREE.Vector3(3, 4, 0), new THREE.Vector3(8, 9, 0)) + + const cloned = entity.fastDeepClone() + + expectWcsBboxCloseTo(cloned.wcsBbox, [3, 4, 0], [8, 9, 0]) + }) +}) diff --git a/packages/three-renderer/__tests__/AcTrGroup.spec.ts b/packages/three-renderer/__tests__/AcTrGroup.spec.ts new file mode 100644 index 00000000..33514635 --- /dev/null +++ b/packages/three-renderer/__tests__/AcTrGroup.spec.ts @@ -0,0 +1,112 @@ +import { AcGeMatrix3d } from '@mlightcad/data-model' +import * as THREE from 'three' + +import { expectWcsBboxCloseTo } from './helpers/expectWcsBbox' +import { AcTrGroup } from '../src/object/AcTrGroup' +import { AcTrLine } from '../src/object/AcTrLine' +import { AcTrRenderContext } from '../src/renderer/AcTrRenderContext' +import { AcTrSubEntityTraitsUtil } from '../src/util' + +const defaultTraits = AcTrSubEntityTraitsUtil.createDefaultTraits() + +function createLine( + objectId: string, + start: { x: number; y: number }, + end: { x: number; y: number }, + context: AcTrRenderContext, + layerName = '0' +) { + const line = new AcTrLine( + [ + { x: start.x, y: start.y, z: 0 }, + { x: end.x, y: end.y, z: 0 } + ], + defaultTraits, + context, + false + ) + line.objectId = objectId + line.layerName = layerName + line.userData.layerName = layerName + return line +} + +describe('AcTrGroup wcsBbox', () => { + it('unions child wcsBbox values into the group wcsBbox', () => { + const context = new AcTrRenderContext() + const lineA = createLine('line-a', { x: 0, y: 0 }, { x: 10, y: 0 }, context) + const lineB = createLine('line-b', { x: 2, y: 5 }, { x: 8, y: 15 }, context) + + const group = new AcTrGroup([lineA, lineB], context) + + expectWcsBboxCloseTo(group.wcsBbox, [0, 0, 0], [10, 15, 0]) + }) + + it('stores per-child WCS boxes for spatial indexing', () => { + const context = new AcTrRenderContext() + const lineA = createLine('line-a', { x: 0, y: 0 }, { x: 10, y: 0 }, context) + const lineB = createLine('line-b', { x: 2, y: 5 }, { x: 8, y: 15 }, context) + + const group = new AcTrGroup([lineA, lineB], context) + + expect(group.wcsChildBoxes).toEqual([ + { minX: 0, minY: 0, maxX: 10, maxY: 0, id: 'line-a' }, + { minX: 2, minY: 5, maxX: 8, maxY: 15, id: 'line-b' } + ]) + }) + + it('transforms group and child WCS boxes together via applyMatrix', () => { + const context = new AcTrRenderContext() + const line = createLine('line-a', { x: 0, y: 0 }, { x: 10, y: 5 }, context) + const group = new AcTrGroup([line], context) + + group.applyMatrix(new AcGeMatrix3d().makeTranslation(50, 25, 0)) + + expectWcsBboxCloseTo(group.wcsBbox, [50, 25, 0], [60, 30, 0]) + expect(group.wcsChildBoxes[0]).toMatchObject({ + minX: 50, + minY: 25, + maxX: 60, + maxY: 30, + id: 'line-a' + }) + }) + + it('keeps wcsBbbox aligned with wcsChildBoxes union after insert transform', () => { + const context = new AcTrRenderContext() + const line0 = createLine('line-0', { x: 0, y: 0 }, { x: 10, y: 0 }, context, '0') + const lineL2 = createLine( + 'line-l2', + { x: 5, y: 5 }, + { x: 15, y: 10 }, + context, + 'L2' + ) + + const group = new AcTrGroup([line0, lineL2], context) + expect(group.isOnTheSameLayer).toBe(false) + + group.applyMatrix(new AcGeMatrix3d().makeTranslation(100, 200, 0)) + + expectWcsBboxCloseTo(group.wcsBbox, [100, 200, 0], [115, 210, 0]) + expect(group.wcsChildBoxes).toEqual([ + { minX: 100, minY: 200, maxX: 110, maxY: 200, id: 'line-0' }, + { minX: 105, minY: 205, maxX: 115, maxY: 210, id: 'line-l2' } + ]) + + const union = new THREE.Box3() + for (const box of group.wcsChildBoxes) { + union.union( + new THREE.Box3( + new THREE.Vector3(box.minX, box.minY, 0), + new THREE.Vector3(box.maxX, box.maxY, 0) + ) + ) + } + expectWcsBboxCloseTo(group.wcsBbox, [union.min.x, union.min.y, 0], [ + union.max.x, + union.max.y, + 0 + ]) + }) +}) diff --git a/packages/three-renderer/__tests__/AcTrImage.spec.ts b/packages/three-renderer/__tests__/AcTrImage.spec.ts index 979e1fae..220339e6 100644 --- a/packages/three-renderer/__tests__/AcTrImage.spec.ts +++ b/packages/three-renderer/__tests__/AcTrImage.spec.ts @@ -1,7 +1,44 @@ +import * as THREE from 'three' + +import { expectWcsBboxEmpty } from './helpers/expectWcsBbox' import { AcTrImage } from '../src/object/AcTrImage' +import { AcTrRenderContext } from '../src/renderer/AcTrRenderContext' describe('AcTrImage', () => { it('always unbatches without consulting policy', () => { expect(AcTrImage.prototype.resolveDrawMode.call({})).toBe('unbatch') }) }) + +describe('AcTrImage wcsBbox', () => { + beforeEach(() => { + jest.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-image') + jest.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined) + jest + .spyOn(THREE.TextureLoader.prototype, 'load') + .mockImplementation(function load(this: THREE.TextureLoader) { + return new THREE.Texture() + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('leaves wcsBbox empty because raster bounds are tracked separately', () => { + const image = new AcTrImage( + new Blob(['image-bytes'], { type: 'image/png' }), + { + boundary: [ + { x: 0, y: 0 }, + { x: 10, y: 0 }, + { x: 10, y: 5 }, + { x: 0, y: 5 } + ] + } as never, + new AcTrRenderContext() + ) + + expectWcsBboxEmpty(image.wcsBbox) + }) +}) \ No newline at end of file diff --git a/packages/three-renderer/__tests__/AcTrLine.spec.ts b/packages/three-renderer/__tests__/AcTrLine.spec.ts index 41dbbef5..3e188f3f 100644 --- a/packages/three-renderer/__tests__/AcTrLine.spec.ts +++ b/packages/three-renderer/__tests__/AcTrLine.spec.ts @@ -4,6 +4,7 @@ import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js' import { AcTrBatchedGroup } from '../src/batch/AcTrBatchedGroup' import type { AcTrBatchDrawPolicy } from '../src/draw/AcTrBatchDrawPolicy' import { RTE_REBASE_THRESHOLD } from '../src/draw/AcTrBatchDrawPolicy' +import { expectWcsBboxCloseTo } from './helpers/expectWcsBbox' import { AcTrLine } from '../src/object/AcTrLine' import { AcTrRenderContext } from '../src/renderer/AcTrRenderContext' import { AcTrStyleManager } from '../src/style/AcTrStyleManager' @@ -134,3 +135,37 @@ describe('AcTrLine', () => { expect(drawable.position.x).toBeCloseTo(largeX + 50, 0) }) }) + +describe('AcTrLine wcsBbox', () => { + it('stores endpoint bounds in world coordinates', () => { + const line = new AcTrLine( + [ + { x: 3, y: 4, z: 0 }, + { x: 13, y: 14, z: 0 } + ], + defaultTraits, + new AcTrRenderContext(), + false + ) + + expectWcsBboxCloseTo(line.wcsBbox, [3, 4, 0], [13, 14, 0]) + }) + + it('keeps wcsBbox in world coordinates when geometry is rebased locally', () => { + const line = new AcTrLine( + [ + { x: largeX, y: 2_000_000, z: 0 }, + { x: largeX + 100, y: 2_000_050, z: 0 } + ], + defaultTraits, + new AcTrRenderContext() + ) + + expectWcsBboxCloseTo( + line.wcsBbox, + [largeX, 2_000_000, 0], + [largeX + 100, 2_000_050, 0], + 0 + ) + }) +}) diff --git a/packages/three-renderer/__tests__/AcTrLineSegments.spec.ts b/packages/three-renderer/__tests__/AcTrLineSegments.spec.ts index 3c96c217..54b9a77c 100644 --- a/packages/three-renderer/__tests__/AcTrLineSegments.spec.ts +++ b/packages/three-renderer/__tests__/AcTrLineSegments.spec.ts @@ -3,6 +3,7 @@ import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js' import { AcTrBatchedGroup } from '../src/batch/AcTrBatchedGroup' import { RTE_REBASE_THRESHOLD } from '../src/draw/AcTrBatchDrawPolicy' +import { expectWcsBboxCloseTo } from './helpers/expectWcsBbox' import { AcTrLineSegments } from '../src/object/AcTrLineSegments' import { AcTrRenderContext } from '../src/renderer/AcTrRenderContext' import { AcTrSubEntityTraitsUtil } from '../src/util' @@ -95,3 +96,43 @@ describe('AcTrLineSegments', () => { expect(drawable.position.x).toBeCloseTo(largeX + 40, 0) }) }) + +describe('AcTrLineSegments wcsBbox', () => { + it('stores segment bounds in world coordinates', () => { + const array = new Float32Array([1, 2, 0, 11, 22, 0]) + const lineEntity = new AcTrLineSegments( + array, + 3, + new Uint16Array([0, 1]), + defaultTraits, + new AcTrRenderContext() + ) + + expectWcsBboxCloseTo(lineEntity.wcsBbox, [1, 2, 0], [11, 22, 0]) + }) + + it('keeps wcsBbox in world coordinates when geometry is rebased locally', () => { + const array = new Float32Array([ + largeX, + 3_000_000, + 0, + largeX + 80, + 3_000_040, + 0 + ]) + const lineEntity = new AcTrLineSegments( + array, + 3, + new Uint16Array([0, 1]), + defaultTraits, + new AcTrRenderContext() + ) + + expectWcsBboxCloseTo( + lineEntity.wcsBbox, + [largeX, 3_000_000, 0], + [largeX + 80, 3_000_040, 0], + 0 + ) + }) +}) diff --git a/packages/three-renderer/__tests__/AcTrMText.spec.ts b/packages/three-renderer/__tests__/AcTrMText.spec.ts index 78f71413..2e5dc533 100644 --- a/packages/three-renderer/__tests__/AcTrMText.spec.ts +++ b/packages/three-renderer/__tests__/AcTrMText.spec.ts @@ -18,14 +18,14 @@ import { } from '../src/util/AcTrObjectUserData' type GeometryHost = THREE.Object3D & { - box: THREE.Box3 + wcsBbox: THREE.Box3 computeGeometryBox: () => THREE.Box3 hasGeometry: (object: THREE.Object3D) => boolean } type RaycastHost = THREE.Object3D & { _mtext?: Pick - box: THREE.Box3 + wcsBbox: THREE.Box3 } const privateMethods = AcTrMText.prototype as unknown as { @@ -48,7 +48,7 @@ const batchPolicy: AcTrBatchDrawPolicy = { resolveDrawMode: () => 'batch' } -describe('AcTrMText', () => { +describe('AcTrMText wcsBbox', () => { it('computes the selection box from transformed rendered child geometry', () => { const host = createGeometryHost() host.add(createBoxMesh({ x: 10, y: 20, z: 0 })) @@ -72,8 +72,8 @@ describe('AcTrMText', () => { createMTextObject(logicalMTextBox) ) - expect(host.box.min.toArray()).toEqual([10, 18, 0]) - expect(host.box.max.toArray()).toEqual([14, 24, 0]) + expect(host.wcsBbox.min.toArray()).toEqual([10, 18, 0]) + expect(host.wcsBbox.max.toArray()).toEqual([14, 24, 0]) }) it('ignores a disjoint renderer-provided mtext box', () => { @@ -89,8 +89,8 @@ describe('AcTrMText', () => { createMTextObject(offsetMTextBox) ) - expect(host.box.min.toArray()).toEqual([10, 20, 0]) - expect(host.box.max.toArray()).toEqual([14, 22, 0]) + expect(host.wcsBbox.min.toArray()).toEqual([10, 20, 0]) + expect(host.wcsBbox.max.toArray()).toEqual([14, 22, 0]) }) it('falls back to the renderer-provided mtext box when there is no child geometry', () => { @@ -102,10 +102,12 @@ describe('AcTrMText', () => { privateMethods.updateSelectionBox.call(host, createMTextObject(fallbackBox)) - expect(host.box.min.toArray()).toEqual([100, 100, 0]) - expect(host.box.max.toArray()).toEqual([101, 101, 0]) + expect(host.wcsBbox.min.toArray()).toEqual([100, 100, 0]) + expect(host.wcsBbox.max.toArray()).toEqual([101, 101, 0]) }) +}) +describe('AcTrMText', () => { it('keeps precise mtext raycast hits when the renderer reports an intersection', () => { const raycaster = createRaycaster() const hitPoint = new THREE.Vector3(0, 0, 0) @@ -115,7 +117,7 @@ describe('AcTrMText', () => { object: new THREE.Object3D() } const host = createRaycastHost({ - box: createSelectableBox(), + wcsBbox: createSelectableBox(), mtextRaycast: (_raycaster, intersects) => { intersects.push(rendererHit) } @@ -130,7 +132,7 @@ describe('AcTrMText', () => { it('uses the entity selection box as a raycast fallback when mtext layout misses', () => { const raycaster = createRaycaster() const host = createRaycastHost({ - box: createSelectableBox(), + wcsBbox: createSelectableBox(), mtextRaycast: jest.fn() }) const intersects: THREE.Intersection[] = [] @@ -147,7 +149,7 @@ describe('AcTrMText', () => { it('does not report a fallback raycast hit when the selection box is empty', () => { const raycaster = createRaycaster() const host = createRaycastHost({ - box: new THREE.Box3(), + wcsBbox: new THREE.Box3(), mtextRaycast: jest.fn() }) const intersects: THREE.Intersection[] = [] @@ -217,18 +219,18 @@ describe('AcTrMText', () => { function createGeometryHost(): GeometryHost { const host = new THREE.Object3D() as GeometryHost - host.box = new THREE.Box3() + host.wcsBbox = new THREE.Box3() host.computeGeometryBox = privateMethods.computeGeometryBox host.hasGeometry = privateMethods.hasGeometry return host } function createRaycastHost(options: { - box: THREE.Box3 + wcsBbox: THREE.Box3 mtextRaycast: Pick['raycast'] }): RaycastHost { const host = new THREE.Object3D() as RaycastHost - host.box = options.box + host.wcsBbox = options.wcsBbox host._mtext = { raycast: options.mtextRaycast } diff --git a/packages/three-renderer/__tests__/AcTrPoint.spec.ts b/packages/three-renderer/__tests__/AcTrPoint.spec.ts new file mode 100644 index 00000000..f31850da --- /dev/null +++ b/packages/three-renderer/__tests__/AcTrPoint.spec.ts @@ -0,0 +1,36 @@ +import { expectWcsBboxCloseTo } from './helpers/expectWcsBbox' +import { AcTrPoint } from '../src/object/AcTrPoint' +import { AcTrRenderContext } from '../src/renderer/AcTrRenderContext' +import { AcTrSubEntityTraitsUtil } from '../src/util' + +const defaultTraits = AcTrSubEntityTraitsUtil.createDefaultTraits() +const dotStyle = { displayMode: 0, displaySize: 0 } +const crossStyle = { displayMode: 2, displaySize: 0 } + +describe('AcTrPoint wcsBbox', () => { + it('stores the point location in wcsBbox for dot display mode', () => { + const point = new AcTrPoint( + { x: 12, y: 34, z: 0 }, + defaultTraits, + dotStyle, + new AcTrRenderContext() + ) + + expectWcsBboxCloseTo(point.wcsBbox, [12, 34, 0], [12, 34, 0]) + }) + + it('includes symbol geometry in wcsBbox for marker display mode', () => { + const point = new AcTrPoint( + { x: 100, y: 200, z: 0 }, + defaultTraits, + crossStyle, + new AcTrRenderContext() + ) + + expect(point.wcsBbox.isEmpty()).toBe(false) + expect(point.wcsBbox.min.x).toBeLessThanOrEqual(100) + expect(point.wcsBbox.max.x).toBeGreaterThanOrEqual(100) + expect(point.wcsBbox.min.y).toBeLessThanOrEqual(200) + expect(point.wcsBbox.max.y).toBeGreaterThanOrEqual(200) + }) +}) diff --git a/packages/three-renderer/__tests__/AcTrPolygon.spec.ts b/packages/three-renderer/__tests__/AcTrPolygon.spec.ts new file mode 100644 index 00000000..7c9403ad --- /dev/null +++ b/packages/three-renderer/__tests__/AcTrPolygon.spec.ts @@ -0,0 +1,41 @@ +import { AcGeArea2d, AcGePoint2d } from '@mlightcad/data-model' + +import { expectWcsBboxCloseTo } from './helpers/expectWcsBbox' +import { AcTrPolygon } from '../src/object/AcTrPolygon' +import { AcTrRenderContext } from '../src/renderer/AcTrRenderContext' +import { AcTrSubEntityTraitsUtil } from '../src/util' + +const defaultTraits = AcTrSubEntityTraitsUtil.createDefaultTraits() + +function createRectangularArea( + minX: number, + minY: number, + maxX: number, + maxY: number +): AcGeArea2d { + const loop = [ + new AcGePoint2d(minX, minY), + new AcGePoint2d(maxX, minY), + new AcGePoint2d(maxX, maxY), + new AcGePoint2d(minX, maxY) + ] + + return { + getPoints: () => [loop], + buildHierarchy: () => ({ + children: [{ index: 0, children: [] }] + }) + } as unknown as AcGeArea2d +} + +describe('AcTrPolygon wcsBbox', () => { + it('stores the filled hatch bounds in wcsBbox', () => { + const polygon = new AcTrPolygon( + createRectangularArea(5, 10, 25, 30), + defaultTraits, + new AcTrRenderContext() + ) + + expectWcsBboxCloseTo(polygon.wcsBbox, [5, 10, 0], [25, 30, 0]) + }) +}) diff --git a/packages/three-renderer/__tests__/AcTrShape.spec.ts b/packages/three-renderer/__tests__/AcTrShape.spec.ts new file mode 100644 index 00000000..0417a349 --- /dev/null +++ b/packages/three-renderer/__tests__/AcTrShape.spec.ts @@ -0,0 +1,94 @@ +import type { MTextObject } from '@mlightcad/mtext-renderer' +import * as THREE from 'three' + +jest.mock('../src/renderer', () => ({ + AcTrMTextRenderer: { + getInstance: jest.fn() + } +})) + +import { AcTrMTextRenderer } from '../src/renderer' +import { expectWcsBboxCloseTo } from './helpers/expectWcsBbox' +import { AcTrShape } from '../src/object/AcTrShape' +import { AcTrRenderContext } from '../src/renderer/AcTrRenderContext' +import { AcTrStyleManager } from '../src/style/AcTrStyleManager' + +const privateMethods = AcTrShape.prototype as unknown as { + computeGeometryBox(this: THREE.Object3D): THREE.Box3 + hasGeometry(object: THREE.Object3D): boolean + updateSelectionBox( + this: THREE.Object3D & { wcsBbox: THREE.Box3 }, + rendered: MTextObject + ): void +} + +describe('AcTrShape wcsBbox', () => { + it('uses rendered child geometry bounds in wcsBbox', () => { + const host = createGeometryHost() + host.add(createBoxMesh({ x: 20, y: 30, z: 0 })) + + privateMethods.updateSelectionBox.call(host, createShapeObject(new THREE.Box3())) + + expectWcsBboxCloseTo(host.wcsBbox, [20, 30, 0], [24, 32, 0]) + }) + + it('builds wcsBbox from syncRenderShape output', () => { + const placementRoot = createPlacementRoot({ x: 5, y: 15, z: 0 }) + const rendered = createShapeObject(new THREE.Box3(), placementRoot) + jest.mocked(AcTrMTextRenderer.getInstance).mockReturnValue({ + syncRenderShape: () => rendered + } as never) + + const shape = new AcTrShape( + { name: 'TEST', position: { x: 5, y: 15, z: 0 } } as never, + { layer: '0', color: 7 } as never, + {} as never, + new AcTrRenderContext(new AcTrStyleManager()) + ) + + expectWcsBboxCloseTo(shape.wcsBbox, [5, 15, 0], [9, 17, 0]) + }) +}) + +function createGeometryHost() { + const host = new THREE.Object3D() as THREE.Object3D & { + wcsBbox: THREE.Box3 + computeGeometryBox: () => THREE.Box3 + hasGeometry: (object: THREE.Object3D) => boolean + } + host.wcsBbox = new THREE.Box3() + host.computeGeometryBox = privateMethods.computeGeometryBox + host.hasGeometry = privateMethods.hasGeometry + return host +} + +function createShapeObject( + box: THREE.Box3, + root: THREE.Object3D = new THREE.Object3D() +): MTextObject { + const rendered = root as MTextObject + rendered.box = box + rendered.createLayoutData = () => ({ lines: [], chars: [] }) + return rendered +} + +function createBoxMesh(position: THREE.Vector3Like) { + const geometry = new THREE.BufferGeometry() + geometry.setAttribute( + 'position', + new THREE.Float32BufferAttribute([0, 0, 0, 4, 0, 0, 4, 2, 0, 0, 2, 0], 3) + ) + geometry.setIndex([0, 1, 2, 0, 2, 3]) + + const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial()) + mesh.position.copy(position as THREE.Vector3) + return mesh +} + +function createPlacementRoot(insertion: THREE.Vector3Like) { + const placementRoot = new THREE.Group() + placementRoot.position.set(insertion.x, insertion.y, insertion.z ?? 0) + placementRoot.add(createBoxMesh({ x: 0, y: 0, z: 0 })) + placementRoot.updateMatrixWorld(true) + return placementRoot +} diff --git a/packages/three-renderer/__tests__/helpers/expectWcsBbox.ts b/packages/three-renderer/__tests__/helpers/expectWcsBbox.ts new file mode 100644 index 00000000..0e11912f --- /dev/null +++ b/packages/three-renderer/__tests__/helpers/expectWcsBbox.ts @@ -0,0 +1,21 @@ +import * as THREE from 'three' + +export type WcsBboxCorner = readonly [number, number, number?] + +export function expectWcsBboxCloseTo( + box: THREE.Box3, + min: WcsBboxCorner, + max: WcsBboxCorner, + precision = 5 +) { + expect(box.min.x).toBeCloseTo(min[0], precision) + expect(box.min.y).toBeCloseTo(min[1], precision) + expect(box.min.z).toBeCloseTo(min[2] ?? 0, precision) + expect(box.max.x).toBeCloseTo(max[0], precision) + expect(box.max.y).toBeCloseTo(max[1], precision) + expect(box.max.z).toBeCloseTo(max[2] ?? 0, precision) +} + +export function expectWcsBboxEmpty(box: THREE.Box3) { + expect(box.isEmpty()).toBe(true) +} diff --git a/packages/three-renderer/src/draw/AcTrBatchDrawPolicy.ts b/packages/three-renderer/src/draw/AcTrBatchDrawPolicy.ts index 5b0a0f0f..ceb7f071 100644 --- a/packages/three-renderer/src/draw/AcTrBatchDrawPolicy.ts +++ b/packages/three-renderer/src/draw/AcTrBatchDrawPolicy.ts @@ -143,8 +143,7 @@ export function resolveAnchorFromPoints( * Empty boxes yield `undefined`, which causes coordinate-based policies to * fall back to `'batch'`. * - * @param box - Entity geometry bounds in drawing/world coordinates (without - * entity transform applied). + * @param box - Entity WCS bounding box. * @returns Bounding-box center, or `undefined` when `box` is empty. */ export function resolveAnchorFromBox( diff --git a/packages/three-renderer/src/object/AcTrEntity.ts b/packages/three-renderer/src/object/AcTrEntity.ts index 1d1fd122..fc6f332b 100644 --- a/packages/three-renderer/src/object/AcTrEntity.ts +++ b/packages/three-renderer/src/object/AcTrEntity.ts @@ -21,12 +21,12 @@ import { AcTrObject } from './AcTrObject' */ export class AcTrEntity extends AcTrObject implements AcGiEntity { declare userData: AcTrEntityUserData - protected _box: THREE.Box3 + protected _wcsBbox: THREE.Box3 protected _basePoint?: AcGePoint3d constructor(context: AcTrRenderContext) { super(context) - this._box = new THREE.Box3() + this._wcsBbox = new THREE.Box3() } /** @@ -46,15 +46,17 @@ export class AcTrEntity extends AcTrObject implements AcGiEntity { } /** - * The bounding box without considering transformation matrix applied on this object. - * If you want to get bounding box with transformation matrix, please call `applyMatrix4` - * for this box. + * Axis-aligned bounding box in world (WCS) coordinates. + * + * Used for spatial indexing, selection, and raycast fallback. Subclasses must + * populate this in WCS when geometry is built; {@link applyMatrix} updates it + * when a block or insert transform is applied. */ - get box() { - return this._box + get wcsBbox() { + return this._wcsBbox } - set box(box: THREE.Box3) { - this._box.copy(box) + set wcsBbox(box: THREE.Box3) { + this._wcsBbox.copy(box) } /** @@ -342,7 +344,7 @@ export class AcTrEntity extends AcTrObject implements AcGiEntity { const threeMatrix = AcTrMatrixUtil.createMatrix4(matrix) this.applyMatrix4(threeMatrix) this.updateMatrixWorld(true) - this._box.applyMatrix4(threeMatrix) + this._wcsBbox.applyMatrix4(threeMatrix) } /** @@ -423,7 +425,7 @@ export class AcTrEntity extends AcTrObject implements AcGiEntity { this.objectId = object.objectId this.ownerId = object.ownerId this.layerName = object.layerName - this.box = object.box + this.wcsBbox = object.wcsBbox return super.copy(object, recursive) } diff --git a/packages/three-renderer/src/object/AcTrGroup.ts b/packages/three-renderer/src/object/AcTrGroup.ts index 5d5f5a29..f8e6cd64 100644 --- a/packages/three-renderer/src/object/AcTrGroup.ts +++ b/packages/three-renderer/src/object/AcTrGroup.ts @@ -18,7 +18,7 @@ export interface AcTrEntityBox { */ export class AcTrGroup extends AcTrEntity { private _isOnTheSameLayer: boolean - private _boxes: AcTrEntityBox[] = [] + private _wcsChildBoxes: AcTrEntityBox[] = [] constructor(entities: AcTrEntity[], context: AcTrRenderContext) { super(context) @@ -27,10 +27,10 @@ export class AcTrGroup extends AcTrEntity { if (Array.isArray(entity)) { const subGroup = new AcTrEntity(context) this.add(subGroup) - this.box.union(subGroup.box) + this.wcsBbox.union(subGroup.wcsBbox) } else { this.add(entity) - this.box.union(entity.box) + this.wcsBbox.union(entity.wcsBbox) } this.storeBoxes(entity) }) @@ -89,8 +89,9 @@ export class AcTrGroup extends AcTrEntity { return this._isOnTheSameLayer } - get boxes() { - return this._boxes + /** Per-child WCS bounding boxes used by the spatial index. */ + get wcsChildBoxes() { + return this._wcsChildBoxes } /** @@ -98,7 +99,9 @@ export class AcTrGroup extends AcTrEntity { */ applyMatrix(matrix: AcGeMatrix3d) { const threeMatrix = AcTrMatrixUtil.createMatrix4(matrix) - this._boxes.forEach(box => this.applyMatrixToEntityBox(box, threeMatrix)) + this._wcsChildBoxes.forEach(box => + this.applyMatrixToEntityBox(box, threeMatrix) + ) super.applyMatrix(matrix) } @@ -107,8 +110,8 @@ export class AcTrGroup extends AcTrEntity { */ copy(object: AcTrGroup, recursive?: boolean) { this._isOnTheSameLayer = object._isOnTheSameLayer - this._boxes = [] - object.boxes.forEach(box => this._boxes.push({ ...box })) + this._wcsChildBoxes = [] + object.wcsChildBoxes.forEach(box => this._wcsChildBoxes.push({ ...box })) return super.copy(object, recursive) } @@ -124,14 +127,14 @@ export class AcTrGroup extends AcTrEntity { private storeBoxes(object: THREE.Object3D) { if (object instanceof AcTrGroup) { - object._boxes.forEach(box => this._boxes.push(box)) + object._wcsChildBoxes.forEach(box => this._wcsChildBoxes.push(box)) } else if (object instanceof AcTrEntity) { - // only leaf entities should contribute to _boxes - this._boxes.push({ - minX: object.box.min.x, - minY: object.box.min.y, - maxX: object.box.max.x, - maxY: object.box.max.y, + // only leaf entities should contribute to _wcsChildBoxes + this._wcsChildBoxes.push({ + minX: object.wcsBbox.min.x, + minY: object.wcsBbox.min.y, + maxX: object.wcsBbox.max.x, + maxY: object.wcsBbox.max.y, id: object.objectId }) } diff --git a/packages/three-renderer/src/object/AcTrLine.ts b/packages/three-renderer/src/object/AcTrLine.ts index 1b566ea0..abe779d5 100644 --- a/packages/three-renderer/src/object/AcTrLine.ts +++ b/packages/three-renderer/src/object/AcTrLine.ts @@ -90,7 +90,7 @@ export class AcTrLine extends AcTrEntity { override resolveDrawMode(): AcTrDrawMode { return this.batchDrawPolicy.resolveDrawMode({ - anchor: resolveAnchorFromBox(this.box) + anchor: resolveAnchorFromBox(this.wcsBbox) }) } @@ -104,7 +104,7 @@ export class AcTrLine extends AcTrEntity { } const worldBox = boundingBox.clone() worldBox.translate(localOrigin) - this.box = worldBox + this.wcsBbox = worldBox } private computeLocalOrigin(points: AcGePoint3dLike[]) { diff --git a/packages/three-renderer/src/object/AcTrLineSegments.ts b/packages/three-renderer/src/object/AcTrLineSegments.ts index b44d00bb..313b6bb9 100644 --- a/packages/three-renderer/src/object/AcTrLineSegments.ts +++ b/packages/three-renderer/src/object/AcTrLineSegments.ts @@ -86,7 +86,7 @@ export class AcTrLineSegments extends AcTrEntity { override resolveDrawMode(): AcTrDrawMode { return this.batchDrawPolicy.resolveDrawMode({ - anchor: resolveAnchorFromBox(this.box) + anchor: resolveAnchorFromBox(this.wcsBbox) }) } @@ -100,7 +100,7 @@ export class AcTrLineSegments extends AcTrEntity { } const worldBox = boundingBox.clone() worldBox.translate(localOrigin) - this.box = worldBox + this.wcsBbox = worldBox } } diff --git a/packages/three-renderer/src/object/AcTrMText.ts b/packages/three-renderer/src/object/AcTrMText.ts index cc4cce95..1677818f 100644 --- a/packages/three-renderer/src/object/AcTrMText.ts +++ b/packages/three-renderer/src/object/AcTrMText.ts @@ -124,12 +124,12 @@ export class AcTrMText extends AcTrEntity { // a hit, keep that result because it is usually more precise than the // entity-level fallback below. this._mtext?.raycast(raycaster, intersects) - if (intersects.length > previousLength || this.box.isEmpty()) return + if (intersects.length > previousLength || this.wcsBbox.isEmpty()) return // Fallback path: use the selection box derived from rendered geometry. This // is what protects point selection when the renderer's logical MTEXT box is // shifted by attachment/alignment handling. - _raycastBox.copy(this.box).applyMatrix4(this.matrixWorld) + _raycastBox.copy(this.wcsBbox) if (raycaster.ray.intersectBox(_raycastBox, _raycastPoint)) { intersects.push({ distance: raycaster.ray.origin.distanceTo(_raycastPoint), @@ -198,14 +198,14 @@ export class AcTrMText extends AcTrEntity { private updateSelectionBox(mtext: MTextObject) { const geometryBox = this.computeGeometryBox() if (geometryBox.isEmpty()) { - this.box = mtext.box + this.wcsBbox = mtext.box return } if (!mtext.box.isEmpty() && mtext.box.intersectsBox(geometryBox)) { - this.box = geometryBox.clone().union(mtext.box) + this.wcsBbox = geometryBox.clone().union(mtext.box) return } - this.box = geometryBox + this.wcsBbox = geometryBox } /** diff --git a/packages/three-renderer/src/object/AcTrPoint.ts b/packages/three-renderer/src/object/AcTrPoint.ts index 403d81c6..d3eedc8b 100644 --- a/packages/three-renderer/src/object/AcTrPoint.ts +++ b/packages/three-renderer/src/object/AcTrPoint.ts @@ -41,7 +41,7 @@ export class AcTrPoint extends AcTrEntity { pointSymbol.point ?? new THREE.BufferGeometry().setFromPoints([_vector3.copy(point)]) AcTrBufferGeometryUtil.safeComputeBoundingBox(geometry) - if (geometry.boundingBox) this.box.union(geometry.boundingBox) + if (geometry.boundingBox) this.wcsBbox.union(geometry.boundingBox) const material = this.styleManager.getPointsMaterial(traits) const pointObj = new THREE.Points(geometry, material) // Add the flag to check intersection using bounding box of the mesh @@ -52,7 +52,7 @@ export class AcTrPoint extends AcTrEntity { if (pointSymbol.line) { const geometry = pointSymbol.line AcTrBufferGeometryUtil.safeComputeBoundingBox(geometry) - if (geometry.boundingBox) this.box.union(geometry.boundingBox) + if (geometry.boundingBox) this.wcsBbox.union(geometry.boundingBox) const material = this.styleManager.getLineMaterial(traits, true) const lineSegmentsObj = new THREE.LineSegments(geometry, material) const lineDrawable = getSceneDrawableUserData(lineSegmentsObj) diff --git a/packages/three-renderer/src/object/AcTrPolygon.ts b/packages/three-renderer/src/object/AcTrPolygon.ts index bca8311f..64a4d45a 100644 --- a/packages/three-renderer/src/object/AcTrPolygon.ts +++ b/packages/three-renderer/src/object/AcTrPolygon.ts @@ -66,15 +66,15 @@ export class AcTrPolygon extends AcTrEntity { geometry.dispose() return } - this.box = boundingBox + this.wcsBbox = boundingBox this.addGradientPositionAttribute(geometry, traits) const gradientBounds = { - minX: this.box.min.x, - minY: this.box.min.y, - maxX: this.box.max.x, - maxY: this.box.max.y + minX: this.wcsBbox.min.x, + minY: this.wcsBbox.min.y, + maxX: this.wcsBbox.max.x, + maxY: this.wcsBbox.max.y } const material = this.styleManager.getFillMaterial( traits, @@ -94,7 +94,7 @@ export class AcTrPolygon extends AcTrEntity { return 'unbatch' } return this.batchDrawPolicy.resolveDrawMode({ - anchor: resolveAnchorFromBox(this.box) + anchor: resolveAnchorFromBox(this.wcsBbox) }) } diff --git a/packages/three-renderer/src/object/AcTrShape.ts b/packages/three-renderer/src/object/AcTrShape.ts index 8d8f65fc..0d67dc14 100644 --- a/packages/three-renderer/src/object/AcTrShape.ts +++ b/packages/three-renderer/src/object/AcTrShape.ts @@ -94,9 +94,9 @@ export class AcTrShape extends AcTrEntity { const previousLength = intersects.length this._rendered?.raycast(raycaster, intersects) - if (intersects.length > previousLength || this.box.isEmpty()) return + if (intersects.length > previousLength || this.wcsBbox.isEmpty()) return - _raycastBox.copy(this.box).applyMatrix4(this.matrixWorld) + _raycastBox.copy(this.wcsBbox) if (raycaster.ray.intersectBox(_raycastBox, _raycastPoint)) { intersects.push({ distance: raycaster.ray.origin.distanceTo(_raycastPoint), @@ -137,14 +137,14 @@ export class AcTrShape extends AcTrEntity { private updateSelectionBox(rendered: MTextObject) { const geometryBox = this.computeGeometryBox() if (geometryBox.isEmpty()) { - this.box = rendered.box + this.wcsBbox = rendered.box return } if (!rendered.box.isEmpty() && rendered.box.intersectsBox(geometryBox)) { - this.box = geometryBox.clone().union(rendered.box) + this.wcsBbox = geometryBox.clone().union(rendered.box) return } - this.box = geometryBox + this.wcsBbox = geometryBox } private computeGeometryBox() {