diff --git a/src/camera.ts b/src/camera.ts index 1d9d7a1..bca0142 100644 --- a/src/camera.ts +++ b/src/camera.ts @@ -422,7 +422,7 @@ class Camera extends Element { const pickId = this.pick(sx, sy); if (pickId !== -1) { - splat.getSplatWorldPosition(pickId, vec); + splat.calcSplatWorldPosition(pickId, vec); // create a plane at the world position facing perpendicular to the camera plane.setFromPointNormal(vec, this.entity.forward); diff --git a/src/edit-history.ts b/src/edit-history.ts index 1acc7ca..bee56a4 100644 --- a/src/edit-history.ts +++ b/src/edit-history.ts @@ -1,11 +1,5 @@ import { Events } from './events'; - -interface EditOp { - name: string; - do(): void; - undo(): void; - destroy?(): void; -} +import { EditOp } from './edit-ops'; class EditHistory { history: EditOp[] = []; @@ -76,4 +70,4 @@ class EditHistory { } } -export { EditHistory, EditOp }; +export { EditHistory }; diff --git a/src/edit-ops.ts b/src/edit-ops.ts index f4d12ea..5b4939b 100644 --- a/src/edit-ops.ts +++ b/src/edit-ops.ts @@ -1,23 +1,24 @@ -import { GSplatData, Quat, Vec3 } from 'playcanvas'; -import { Scene } from './scene'; +import { Mat4, Quat, Vec3 } from 'playcanvas'; import { Splat } from './splat'; +import { State } from './splat-state'; -enum State { - selected = 1, - hidden = 2, - deleted = 4 +interface EditOp { + name: string; + do(): void; + undo(): void; + destroy?(): void; } -// build a splat index based on a boolean predicate -const buildIndex = (splatData: GSplatData, pred: (i: number) => boolean) => { - let numSplats = 0; - for (let i = 0; i < splatData.numSplats; ++i) { - if (pred(i)) numSplats++; +// build an index array based on a boolean predicate over indices +const buildIndex = (total: number, pred: (i: number) => boolean) => { + let num = 0; + for (let i = 0; i < total; ++i) { + if (pred(i)) num++; } - const result = new Uint32Array(numSplats); + const result = new Uint32Array(num); let idx = 0; - for (let i = 0; i < splatData.numSplats; ++i) { + for (let i = 0; i < total; ++i) { if (pred(i)) { result[idx++] = i; } @@ -26,36 +27,47 @@ const buildIndex = (splatData: GSplatData, pred: (i: number) => boolean) => { return result; }; -class DeleteSelectionEditOp { - name = 'deleteSelection'; +type filterFunc = (state: number, index: number) => boolean; +type doFunc = (state: number) => number; +type undoFunc = (state: number) => number; + +class StateOp { splat: Splat; indices: Uint32Array; + doIt: doFunc; + undoIt: undoFunc; + updateFlags: number; - constructor(splat: Splat) { + constructor(splat: Splat, filter: filterFunc, doIt: doFunc, undoIt: undoFunc, updateFlags = State.selected) { const splatData = splat.splatData; const state = splatData.getProp('state') as Uint8Array; - const indices = buildIndex(splatData, (i) => !!(state[i] & State.selected)); + const indices = buildIndex(splatData.numSplats, (i) => filter(state[i], i)); this.splat = splat; this.indices = indices; + this.doIt = doIt; + this.undoIt = undoIt; + this.updateFlags = updateFlags; } do() { const splatData = this.splat.splatData; const state = splatData.getProp('state') as Uint8Array; for (let i = 0; i < this.indices.length; ++i) { - state[this.indices[i]] |= State.deleted; + const idx = this.indices[i]; + state[idx] = this.doIt(state[idx]); } - this.splat.updateState(true); + this.splat.updateState(this.updateFlags); } undo() { const splatData = this.splat.splatData; const state = splatData.getProp('state') as Uint8Array; for (let i = 0; i < this.indices.length; ++i) { - state[this.indices[i]] &= ~State.deleted; + const idx = this.indices[i]; + state[idx] = this.undoIt(state[idx]); } - this.splat.updateState(true); + this.splat.updateState(this.updateFlags); } destroy() { @@ -64,41 +76,117 @@ class DeleteSelectionEditOp { } } -class ResetEditOp { - name = 'reset'; - splat: Splat; - indices: Uint32Array; +class SelectAllOp extends StateOp { + name = 'selectAll'; constructor(splat: Splat) { - const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - const indices = buildIndex(splatData, (i) => !!(state[i] & State.deleted)); + super(splat, + (state) => state === 0, + (state) => state | State.selected, + (state) => state & (~State.selected) + ); + } +} - this.splat = splat; - this.indices = indices; +class SelectNoneOp extends StateOp { + name = 'selectNone'; + + constructor(splat: Splat) { + super(splat, + (state) => state === State.selected, + (state) => state & (~State.selected), + (state) => state | State.selected + ); } +} - do() { - const splatData = this.splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - for (let i = 0; i < this.indices.length; ++i) { - state[this.indices[i]] &= ~State.deleted; - } - this.splat.updateState(true); +class SelectInvertOp extends StateOp { + name = 'selectInvert'; + + constructor(splat: Splat) { + super(splat, + (state) => (state & (State.hidden | State.deleted)) === 0, + (state) => state ^ State.selected, + (state) => state ^ State.selected + ); } +} - undo() { - const splatData = this.splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - for (let i = 0; i < this.indices.length; ++i) { - state[this.indices[i]] |= State.deleted; - } - this.splat.updateState(true); +class SelectOp extends StateOp { + name = 'selectOp'; + + constructor(splat: Splat, op: 'add'|'remove'|'set', filter: (i: number) => boolean) { + const filterFunc = { + add: (state: number, index: number) => (state === 0) && filter(index), + remove: (state: number, index: number) => (state === State.selected) && filter(index), + set: (state: number, index: number) => (state === State.selected) !== filter(index), + }; + + const doIt = { + add: (state: number) => state | State.selected, + remove: (state: number) => state & (~State.selected), + set: (state: number) => state ^ State.selected + }; + + const undoIt = { + add: (state: number) => state & (~State.selected), + remove: (state: number) => state | State.selected, + set: (state: number) => state ^ State.selected + }; + + super(splat, filterFunc[op], doIt[op], undoIt[op]); } +} - destroy() { - this.splat = null; - this.indices = null; +class HideSelectionOp extends StateOp { + name = 'hideSelection'; + + constructor(splat: Splat) { + super(splat, + (state) => state === State.selected, + (state) => state | State.hidden, + (state) => state & (~State.hidden), + State.hidden + ); + } +} + +class UnhideAllOp extends StateOp { + name = 'unhideAll'; + + constructor(splat: Splat) { + super(splat, + (state) => (state & (State.hidden | State.deleted)) === State.hidden, + (state) => state & (~State.hidden), + (state) => state | State.hidden, + State.hidden + ); + } +} + +class DeleteSelectionOp extends StateOp { + name = 'deleteSelection'; + + constructor(splat: Splat) { + super(splat, + (state) => state === State.selected, + (state) => state | State.deleted, + (state) => state & (~State.deleted), + State.deleted + ); + } +} + +class ResetOp extends StateOp { + name = 'reset'; + + constructor(splat: Splat) { + super(splat, + (state) => (state & State.deleted) !== 0, + (state) => state & (~State.deleted), + (state) => state | State.deleted, + State.deleted + ); } } @@ -108,41 +196,65 @@ interface EntityTransform { scale?: Vec3; } -interface EntityOp { - splat: Splat; - old: EntityTransform; - new: EntityTransform; -} - class EntityTransformOp { name = 'entityTransform'; - entityOps: EntityOp[]; - constructor(entityOps: EntityOp[]) { - this.entityOps = entityOps; + splat: Splat; + oldt: EntityTransform; + newt: EntityTransform; + + constructor(options: { splat: Splat, oldt: EntityTransform, newt: EntityTransform }) { + this.splat = options.splat; + this.oldt = options.oldt; + this.newt = options.newt; } do() { - this.entityOps.forEach((entityOp) => { - entityOp.splat.move(entityOp.new.position, entityOp.new.rotation, entityOp.new.scale); - }); + this.splat.move(this.newt.position, this.newt.rotation, this.newt.scale); } undo() { - this.entityOps.forEach((entityOp) => { - entityOp.splat.move(entityOp.old.position, entityOp.old.rotation, entityOp.old.scale); - }); + this.splat.move(this.oldt.position, this.oldt.rotation, this.oldt.scale); } destroy() { - this.entityOps = []; + this.splat = null; + this.oldt = null; + this.newt = null; + } +} + +class SetPivotOp { + name = "setPivot"; + splat: Splat; + oldPivot: Vec3; + newPivot: Vec3; + + constructor(splat: Splat, oldPivot: Vec3, newPivot: Vec3) { + this.splat = splat; + this.oldPivot = oldPivot; + this.newPivot = newPivot; + } + + do() { + this.splat.setPivot(this.newPivot); + } + + undo() { + this.splat.setPivot(this.oldPivot); } } export { - State, - DeleteSelectionEditOp, - ResetEditOp, - EntityOp, - EntityTransformOp + EditOp, + SelectAllOp, + SelectNoneOp, + SelectInvertOp, + SelectOp, + HideSelectionOp, + UnhideAllOp, + DeleteSelectionOp, + ResetOp, + EntityTransformOp, + SetPivotOp }; diff --git a/src/editor.ts b/src/editor.ts index 520ab1c..6cd6693 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -8,11 +8,12 @@ import { Scene } from './scene'; import { EditorUI } from './ui/editor'; import { EditHistory } from './edit-history'; import { Splat } from './splat'; -import { State, DeleteSelectionEditOp, ResetEditOp } from './edit-ops'; +import { State } from './splat-state'; +import { SelectAllOp, SelectNoneOp, SelectInvertOp, SelectOp, HideSelectionOp, UnhideAllOp, DeleteSelectionOp, ResetOp } from './edit-ops'; import { Events } from './events'; // register for editor and scene events -const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: Scene, editorUI: EditorUI) => { +const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: Scene) => { const vec = new Vec3(); const vec2 = new Vec3(); const vec4 = new Vec4(); @@ -25,31 +26,6 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S return selected?.visible ? [selected] : []; }; - const processSelection = (state: Uint8Array, op: string, pred: (i: number) => boolean) => { - for (let i = 0; i < state.length; ++i) { - if (state[i] & (State.deleted | State.hidden)) { - state[i] &= ~State.selected; - } else { - const result = pred(i); - switch (op) { - case 'add': - if (result) state[i] |= State.selected; - break; - case 'remove': - if (result) state[i] &= ~State.selected; - break; - case 'set': - if (result) { - state[i] |= State.selected; - } else { - state[i] &= ~State.selected; - } - break; - } - } - } - }; - let lastExportCursor = 0; // add unsaved changes warning message. @@ -206,44 +182,31 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S events.on('select.all', () => { selectedSplats().forEach((splat) => { - const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - processSelection(state, 'set', () => true); - splat.updateState(); + events.fire('edit.add', new SelectAllOp(splat)); }); }); events.on('select.none', () => { selectedSplats().forEach((splat) => { - const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - processSelection(state, 'set', () => false); - splat.updateState(); + events.fire('edit.add', new SelectNoneOp(splat)); }); }); events.on('select.invert', () => { selectedSplats().forEach((splat) => { - const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - processSelection(state, 'set', (i) => !(state[i] & State.selected)); - splat.updateState(); + events.fire('edit.add', new SelectInvertOp(splat)); }); }); events.on('select.pred', (op, pred: (i: number) => boolean) => { selectedSplats().forEach((splat) => { - const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - processSelection(state, op, pred); - splat.updateState(); + events.fire('edit.add', new SelectOp(splat, op, pred)); }); }); - events.on('select.bySphere', (op: string, sphere: number[]) => { + events.on('select.bySphere', (op: 'add'|'remove'|'set', sphere: number[]) => { selectedSplats().forEach((splat) => { const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; const x = splatData.getProp('x'); const y = splatData.getProp('y'); const z = splatData.getProp('z'); @@ -256,48 +219,23 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S mat.invert(splat.worldTransform); mat.transformPoint(vec, vec); - processSelection(state, op, (i) => { - vec2.set(x[i], y[i], z[i]); - return vec2.sub(vec).lengthSq() < radius2; - }); - - splat.updateState(); - }); - }); - - events.on('select.byPlane', (op: string, axis: number[], distance: number) => { - selectedSplats().forEach((splat) => { - const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - const x = splatData.getProp('x'); - const y = splatData.getProp('y'); - const z = splatData.getProp('z'); - - vec.set(axis[0], axis[1], axis[2]); - vec2.set(axis[0] * distance, axis[1] * distance, axis[2] * distance); + const sx = vec.x; + const sy = vec.y; + const sz = vec.z; - // transform the plane to local space - mat.invert(splat.worldTransform); - mat.transformVector(vec, vec); - mat.transformPoint(vec2, vec2); + const filter = (i: number) => { + return (x[i] - sx) ** 2 + (y[i] - sy) ** 2 + (z[i] - sz) ** 2 < radius2; + }; - const localDistance = vec.dot(vec2); - - processSelection(state, op, (i) => { - vec2.set(x[i], y[i], z[i]); - return vec.dot(vec2) - localDistance > 0; - }); - - splat.updateState(); + events.fire('edit.add', new SelectOp(splat, op, filter)); }); }); - events.on('select.rect', (op: string, rect: any) => { + events.on('select.rect', (op: 'add'|'remove'|'set', rect: any) => { const mode = events.invoke('camera.mode'); selectedSplats().forEach((splat) => { const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; if (mode === 'centers') { const px = splatData.getProp('x'); @@ -320,7 +258,7 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S const ex = rect.end.x * 2 - 1; const ey = rect.end.y * 2 - 1; - processSelection(state, op, (i) => { + const filter = (i: number) => { const vx = px[i]; const vy = py[i]; const vz = pz[i]; @@ -340,7 +278,9 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S const z = (vx * m20 + vy * m21 + vz * m22 + m23); return z >= -w && z <= w; - }); + }; + + events.fire('edit.add', new SelectOp(splat, op, filter)); } else if (mode === 'rings') { const { width, height } = scene.targetSize; @@ -351,22 +291,22 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S Math.floor((rect.end.x - rect.start.x) * width), Math.floor((rect.end.y - rect.start.y) * height) ); + const selected = new Set(pick); - processSelection(state, op, (i) => { + const filter = (i: number) => { return selected.has(i); - }); - } + }; - splat.updateState(); + events.fire('edit.add', new SelectOp(splat, op, filter)); + } }); }); - events.on('select.byMask', (op: string, mask: ImageData) => { + events.on('select.byMask', (op: 'add'|'remove'|'set', mask: ImageData) => { const mode = events.invoke('camera.mode'); selectedSplats().forEach((splat) => { const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; if (mode === 'centers') { const px = splatData.getProp('x'); @@ -387,7 +327,7 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S const width = mask.width; const height = mask.height; - processSelection(state, op, (i) => { + const filter = (i: number) => { const vx = px[i]; const vy = py[i]; const vz = pz[i]; @@ -411,7 +351,9 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S const mx = Math.floor((x / w * 0.5 + 0.5) * width); const my = Math.floor((y / w * -0.5 + 0.5) * height); return mask.data[(my * width + mx) * 4] === 255; - }); + }; + + events.fire('edit.add', new SelectOp(splat, op, filter)); } else if (mode === 'rings') { // calculate mask bound so we limit pixel operations let mx0 = mask.width - 1; @@ -451,22 +393,21 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S } } - processSelection(state, op, (i) => { + const filter = (i: number) => { return selected.has(i); - }); - } + }; - splat.updateState(); + events.fire('edit.add', new SelectOp(splat, op, filter)); + } }); }); - events.on('select.point', (op: string, point: { x: number, y: number }) => { + events.on('select.point', (op: 'add'|'remove'|'set', point: { x: number, y: number }) => { const { width, height } = scene.targetSize; const mode = events.invoke('camera.mode'); selectedSplats().forEach((splat) => { const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; if (mode === 'centers') { const x = splatData.getProp('x'); @@ -481,13 +422,15 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S // calculate final matrix mat.mul2(camera.camera._viewProjMat, splat.worldTransform); - processSelection(state, op, (i) => { + const filter = (i: number) => { vec4.set(x[i], y[i], z[i], 1.0); mat.transformVec4(vec4, vec4); const px = (vec4.x / vec4.w * 0.5 + 0.5) * width; const py = (-vec4.y / vec4.w * 0.5 + 0.5) * height; return Math.abs(px - sx) < splatSize && Math.abs(py - sy) < splatSize; - }); + }; + + events.fire('edit.add', new SelectOp(splat, op, filter)); } else if (mode === 'rings') { scene.camera.pickPrep(splat); @@ -496,53 +439,37 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S Math.floor(point.y * height), 1, 1 )[0]; - processSelection(state, op, (i) => { + + const filter = (i: number) => { return i === pickId; - }); - } + }; - splat.updateState(); + events.fire('edit.add', new SelectOp(splat, op, filter)); + } }); }); events.on('select.hide', () => { selectedSplats().forEach((splat) => { - const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - - for (let i = 0; i < state.length; ++i) { - if (state[i] & State.selected) { - state[i] &= ~State.selected; - state[i] |= State.hidden; - } - } - - splat.updateState(); + events.fire('edit.add', new HideSelectionOp(splat)); }); }); events.on('select.unhide', () => { selectedSplats().forEach((splat) => { - const splatData = splat.splatData; - const state = splatData.getProp('state') as Uint8Array; - - for (let i = 0; i < state.length; ++i) { - state[i] &= ~State.hidden; - } - - splat.updateState(); + events.fire('edit.add', new UnhideAllOp(splat)); }); }); events.on('select.delete', () => { selectedSplats().forEach((splat) => { - editHistory.add(new DeleteSelectionEditOp(splat)); + editHistory.add(new DeleteSelectionOp(splat)); }); }); events.on('scene.reset', () => { selectedSplats().forEach((splat) => { - editHistory.add(new ResetEditOp(splat)); + editHistory.add(new ResetOp(splat)); }); }); diff --git a/src/main.ts b/src/main.ts index 1d60de1..cba98de 100644 --- a/src/main.ts +++ b/src/main.ts @@ -145,13 +145,13 @@ const main = async () => { toolManager.register('rectSelection', new RectSelection(events, editorUI.toolsContainer.dom)); toolManager.register('brushSelection', new BrushSelection(events, editorUI.toolsContainer.dom)); toolManager.register('sphereSelection', new SphereSelection(events, scene, editorUI.canvasContainer)); - toolManager.register('move', new MoveTool(events, editHistory, scene)); - toolManager.register('rotate', new RotateTool(events, editHistory, scene)); - toolManager.register('scale', new ScaleTool(events, editHistory, scene)); + toolManager.register('move', new MoveTool(events, scene)); + toolManager.register('rotate', new RotateTool(events, scene)); + toolManager.register('scale', new ScaleTool(events, scene)); window.scene = scene; - registerEditorEvents(events, editHistory, scene, editorUI); + registerEditorEvents(events, editHistory, scene); initSelection(events, scene); initShortcuts(events); await initFileHandler(scene, events, editorUI.appContainer.dom, remoteStorageDetails); diff --git a/src/scene.ts b/src/scene.ts index 93dbe3a..14b0263 100644 --- a/src/scene.ts +++ b/src/scene.ts @@ -15,6 +15,7 @@ import { SceneConfig } from './scene-config'; import { AssetLoader } from './asset-loader'; import { Model } from './model'; import { Splat } from './splat'; +import { SplatDebug } from './splat-debug'; import { Camera } from './camera'; import { CustomShadow as Shadow } from './custom-shadow'; // import { Grid } from './grid'; @@ -44,6 +45,7 @@ class Scene { assetLoader: AssetLoader; camera: Camera; + splatDebug: SplatDebug; shadow: Shadow; grid: Grid; @@ -172,6 +174,9 @@ class Scene { this.camera = new Camera(); this.add(this.camera); + this.splatDebug = new SplatDebug(); + this.add(this.splatDebug); + // this.shadow = new Shadow(); // this.add(this.shadow); diff --git a/src/selection.ts b/src/selection.ts index 69e7c07..ad48058 100644 --- a/src/selection.ts +++ b/src/selection.ts @@ -41,7 +41,7 @@ const initSelection = (events: Events, scene: Scene) => { } }); - events.on('splat.vis', (splat: Splat) => { + events.on('splat.visibility', (splat: Splat) => { if (splat === selection && !splat.visible) { events.fire('selection', null); } diff --git a/src/splat-convert.ts b/src/splat-convert.ts index e0528c9..6976a74 100644 --- a/src/splat-convert.ts +++ b/src/splat-convert.ts @@ -5,7 +5,7 @@ import { Quat, Vec3 } from 'playcanvas'; -import { State } from './edit-ops'; +import { State } from './splat-state'; import { SHRotation } from './sh-utils'; interface ConvertEntry { diff --git a/src/splat-debug.ts b/src/splat-debug.ts index cd34bfb..e470805 100644 --- a/src/splat-debug.ts +++ b/src/splat-debug.ts @@ -1,138 +1,164 @@ import { + createShaderFromCode, BLEND_NORMAL, - Entity, - GSplatData, + BUFFER_STATIC, + PRIMITIVE_POINTS, + SEMANTIC_ATTR13, Material, Mesh, MeshInstance, - PRIMITIVE_POINTS, - SEMANTIC_POSITION, - createShaderFromCode, + TYPE_UINT32, + VertexBuffer, + VertexFormat, } from 'playcanvas'; -import { State } from './edit-ops'; -import { Scene } from './scene'; +import { Splat } from './splat'; +import { ElementType, Element } from './element'; const vs = /* glsl */ ` -attribute vec4 vertex_position; +attribute uint vertex_id; uniform mat4 matrix_model; -uniform mat4 matrix_view; -uniform mat4 matrix_projection; uniform mat4 matrix_viewProjection; +uniform sampler2D splatState; +uniform highp usampler2D splatPosition; uniform float splatSize; +uniform uvec2 texParams; -varying vec4 color; +varying vec4 varying_color; -vec4 colors[3] = vec4[3]( - vec4(0, 0, 0, 0.25), - vec4(0, 0, 1.0, 0.5), - vec4(1.0, 1.0, 0.0, 0.5) -); +// calculate the current splat index and uv +ivec2 calcSplatUV(uint index, uint width) { + return ivec2(int(index % width), int(index / width)); +} void main(void) { - int state = int(vertex_position.w); - if (state == -1) { + ivec2 splatUV = calcSplatUV(vertex_id, texParams.x); + uint splatState = uint(texelFetch(splatState, splatUV, 0).r * 255.0); + + if ((splatState & 6u) != 0u) { + // deleted or hidden (4 or 2) gl_Position = vec4(0.0, 0.0, 2.0, 1.0); + gl_PointSize = 0.0; } else { - gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position.xyz, 1.0); + if ((splatState & 1u) != 0u) { + // selected + varying_color = vec4(1.0, 1.0, 0.0, 0.5); + } else { + varying_color = vec4(0.0, 0.0, 1.0, 0.5); + } + + vec3 p = uintBitsToFloat(texelFetch(splatPosition, splatUV, 0).xyz); + + gl_Position = matrix_viewProjection * matrix_model * vec4(p, 1.0); gl_PointSize = splatSize; - color = colors[state]; } } `; const fs = /* glsl */ ` -varying vec4 color; -void main(void) -{ - gl_FragColor = color; +varying vec4 varying_color; + +void main(void) { + gl_FragColor = varying_color; } `; -class SplatDebug { - splatData: GSplatData; +class SplatDebug extends Element { meshInstance: MeshInstance; - size = 2; - constructor(scene: Scene, root: Entity, splatData: GSplatData) { + constructor() { + super(ElementType.debug); + } + + add() { + const scene = this.scene; const device = scene.graphicsDevice; const shader = createShaderFromCode(device, vs, fs, `splatDebugShader`, { - vertex_position: SEMANTIC_POSITION + vertex_id: SEMANTIC_ATTR13 }); const material = new Material(); material.name = 'splatDebugMaterial'; material.blendType = BLEND_NORMAL; material.shader = shader; - material.update(); - - const x = splatData.getProp('x'); - const y = splatData.getProp('y'); - const z = splatData.getProp('z'); - - const vertexData = new Float32Array(splatData.numSplats * 4); - for (let i = 0; i < splatData.numSplats; ++i) { - vertexData[i * 4 + 0] = x[i]; - vertexData[i * 4 + 1] = y[i]; - vertexData[i * 4 + 2] = z[i]; - vertexData[i * 4 + 3] = 1; - } const mesh = new Mesh(device); - mesh.setPositions(vertexData, 4); - mesh.update(PRIMITIVE_POINTS, true); - this.splatData = splatData; - this.meshInstance = new MeshInstance(mesh, material, root); + const meshInstance = new MeshInstance(mesh, material, null); - this.splatSize = this.size; - } + const events = this.scene.events; - destroy() { - this.meshInstance.material.destroy(); - this.meshInstance.destroy(); - } + const update = (splat: Splat) => { + if (!splat) { + meshInstance.node = null; + return; + } + + const splatData = splat.splatData; - update() { - const splatData = this.splatData; - const s = splatData.getProp('state') as Uint8Array; - - const vb = this.meshInstance.mesh.vertexBuffer; - const vertexData = new Float32Array(vb.lock()); - - let count = 0; - - for (let i = 0; i < splatData.numSplats; ++i) { - if (s[i] & State.deleted) { - // deleted - vertexData[i * 4 + 3] = -1; - } else if (s[i] & State.hidden) { - // hidden - vertexData[i * 4 + 3] = -1; - } else if (!(s[i] & State.selected)) { - // unselected - vertexData[i * 4 + 3] = 1; - } else { - // selected - vertexData[i * 4 + 3] = 2; - count++; + const vertexFormat = new VertexFormat(device, [{ + semantic: SEMANTIC_ATTR13, + components: 1, + type: TYPE_UINT32, + asInt: true + }]); + + // TODO: make use of Splat's mapping instead of rendering all splats + const vertexData = new Uint32Array(splatData.numSplats); + for (let i = 0; i < splatData.numSplats; ++i) { + vertexData[i] = i; } - } - vb.unlock(); + const vertexBuffer = new VertexBuffer(device, vertexFormat, splatData.numSplats, { + usage: BUFFER_STATIC, + data: vertexData + }); + + if (mesh.vertexBuffer) { + mesh.vertexBuffer.destroy(); + mesh.vertexBuffer = null; + } + + mesh.vertexBuffer = vertexBuffer; + mesh.primitive[0] = { + type: PRIMITIVE_POINTS, + base: 0, + count: splatData.numSplats, + }; + + material.setParameter('splatState', splat.stateTexture); + material.setParameter('splatPosition', splat.entity.gsplat.instance.splat.transformATexture); + material.setParameter('texParams', [splat.stateTexture.width, splat.stateTexture.height]); + material.update(); - return count; + meshInstance.node = splat.entity; + }; + + events.on('selection.changed', (selection: Splat) => { + update(selection); + }); + + this.meshInstance = meshInstance; } - set splatSize(splatSize: number) { - this.size = splatSize; - this.meshInstance.material.setParameter('splatSize', splatSize * window.devicePixelRatio); + destroy() { + this.meshInstance.material.destroy(); + this.meshInstance.destroy(); } - get splatSize() { - return this.size; + onPreRender() { + const events = this.scene.events; + const splatSize = events.invoke('camera.splatSize'); + + if (this.meshInstance.node && + splatSize > 0 && + events.invoke('camera.debug') && + events.invoke('camera.mode') === 'centers') { + this.meshInstance.material.setParameter('splatSize', splatSize * window.devicePixelRatio); + this.scene.app.drawMeshInstance(this.meshInstance); + } } } diff --git a/src/splat-state.ts b/src/splat-state.ts new file mode 100644 index 0000000..b319f96 --- /dev/null +++ b/src/splat-state.ts @@ -0,0 +1,7 @@ +enum State { + selected = 1, + hidden = 2, + deleted = 4 +} + +export { State }; diff --git a/src/splat.ts b/src/splat.ts index 1a462a0..844e30d 100644 --- a/src/splat.ts +++ b/src/splat.ts @@ -8,16 +8,14 @@ import { Entity, GSplatData, GSplatResource, - Mat3, Mat4, Quat, Texture, Vec3 } from 'playcanvas'; import { Element, ElementType } from "./element"; -import { SplatDebug } from "./splat-debug"; import { Serializer } from "./serializer"; -import { State } from './edit-ops'; +import { State } from './splat-state'; const vertexShader = /*glsl*/` uniform vec3 view_position; @@ -25,6 +23,8 @@ uniform vec3 view_position; uniform sampler2D splatColor; uniform sampler2D splatState; +uniform mat4 selection_transform; + varying mediump vec2 texCoord; varying mediump vec4 color; flat varying highp uint vertexState; @@ -45,6 +45,14 @@ void main(void) // get center vec3 center = getCenter(); + // get vertex state + vertexState = uint(texelFetch(splatState, splatUV, 0).r * 255.0); + + // apply selection transform + if ((vertexState & 1u) != 0u) { + center = (selection_transform * vec4(center, 1.0)).xyz; + } + // handle transforms mat4 model_view = matrix_view * matrix_model; vec4 splat_cam = model_view * vec4(center, 1.0); @@ -90,8 +98,6 @@ void main(void) id = float(splatId); #endif - vertexState = uint(texelFetch(splatState, splatUV, 0).r * 255.0); - #ifdef PICK_PASS vertexId = splatId; #endif @@ -169,7 +175,6 @@ const vec = new Vec3(); const veca = new Vec3(); const vecb = new Vec3(); const mat = new Mat4(); -const mat3 = new Mat3(); const boundingPoints = [-1, 1].map((x) => { @@ -187,16 +192,22 @@ const boundingPoints = class Splat extends Element { asset: Asset; splatData: GSplatData; - splatDebug: SplatDebug; + numSplats = 0; + numDeleted = 0; + numHidden = 0; + numSelected = 0; pivot: Entity; entity: Entity; changedCounter = 0; stateTexture: Texture; + selectionBoundStorage: BoundingBox; localBoundStorage: BoundingBox; worldBoundStorage: BoundingBox; + selectionBoundDirty = true; localBoundDirty = true; worldBoundDirty = true; visible_ = true; + selectionTransform = new Mat4(); rebuildMaterial: (bands: number) => void; @@ -217,6 +228,7 @@ class Splat extends Element { this.asset = asset; this.splatData = splatData; + this.numSplats = splatData.numSplats; this.pivot = new Entity('splatPivot'); this.entity = splatResource.instantiate(getMaterialOptions(0)); @@ -255,6 +267,7 @@ class Splat extends Element { const material = instance.material; material.setParameter('splatState', this.stateTexture); + material.setParameter('selection_transform', this.selectionTransform.data); material.update(); }; @@ -278,7 +291,7 @@ class Splat extends Element { this.asset.unload(); } - updateState(recalcBound = false) { + updateState(changedState = State.selected) { const state = this.splatData.getProp('state') as Uint8Array; // write state data to gpu texture @@ -286,28 +299,39 @@ class Splat extends Element { data.set(state); this.stateTexture.unlock(); - // update splat debug visual - this.splatDebug.update(); + let numSelected = 0; + let numHidden = 0; + let numDeleted = 0; + + for (let i = 0; i < state.length; ++i) { + const s = state[i]; + if (s & State.deleted) { + numDeleted++; + } else if (s & State.hidden) { + numHidden++; + } else if (s & State.selected) { + numSelected++; + } + } + + this.numSplats = state.length - numDeleted; + this.numHidden = numHidden; + this.numSelected = numSelected; + this.numDeleted = numDeleted; + + this.selectionBoundDirty = true; // handle splats being added or removed - if (recalcBound) { + if (changedState & State.deleted) { this.localBoundDirty = true; this.worldBoundDirty = true; this.scene.boundDirty = true; - // count number of still-visible splats - let numSplats = 0; - for (let i = 0; i < state.length; ++i) { - if ((state[i] & State.deleted) === 0) { - numSplats++; - } - } - let mapping; // create a sorter mapping to remove deleted splats - if (numSplats !== state.length) { - mapping = new Uint32Array(numSplats); + if (this.numSplats !== state.length) { + mapping = new Uint32Array(this.numSplats); let idx = 0; for (let i = 0; i < state.length; ++i) { if ((state[i] & State.deleted) === 0) { @@ -321,8 +345,7 @@ class Splat extends Element { } this.scene.forceRender = true; - - this.scene.events.fire('splat.stateChanged', this); + this.scene.events.fire('splat.stateChanged', this, changedState); } get worldTransform() { @@ -333,7 +356,7 @@ class Splat extends Element { return this.asset.file.filename; } - getSplatWorldPosition(splatId: number, result: Vec3) { + calcSplatWorldPosition(splatId: number, result: Vec3) { if (splatId >= this.splatData.numSplats) { return false; } @@ -350,8 +373,6 @@ class Splat extends Element { } add() { - this.splatDebug = new SplatDebug(this.scene, this.entity, this.splatData); - // add the entity to the scene this.scene.contentRoot.addChild(this.pivot); @@ -361,6 +382,7 @@ class Splat extends Element { this.pivot.setLocalPosition(vec); this.entity.setLocalPosition(-vec.x, -vec.y, -vec.z); + this.selectionBoundDirty = true; this.localBoundDirty = true; this.worldBoundDirty = true; this.scene.boundDirty = true; @@ -370,9 +392,6 @@ class Splat extends Element { } remove() { - this.splatDebug.destroy(); - this.splatDebug = null; - this.scene.events.off('view.bands', this.rebuildMaterial, this); this.scene.contentRoot.removeChild(this.pivot); @@ -396,12 +415,6 @@ class Splat extends Element { material.setParameter('ringSize', (selected && cameraMode === 'rings' && splatSize > 0) ? 0.04 : 0); if (this.visible && selected) { - // render splat centers - if (cameraMode === 'centers' && splatSize > 0) { - this.splatDebug.splatSize = splatSize; - this.scene.app.drawMeshInstance(this.splatDebug.meshInstance); - } - // render bounding box if (events.invoke('camera.bound')) { const bound = this.localBound; @@ -444,6 +457,23 @@ class Splat extends Element { this.scene.events.fire('splat.moved', this); } + // get the selection bound + get selectionBound() { + if (this.selectionBoundDirty) { + const state = this.splatData.getProp('state') as Uint8Array; + const selectionBound = this.selectionBoundStorage; + + const visiblePred = (i: number) => (state[i] & (State.hidden | State.deleted)) === 0; + const selectionPred = (i: number) => visiblePred(i) && ((state[i] & State.selected) === State.selected); + + if (!this.splatData.calcAabb(selectionBound, selectionPred)) { + selectionBound.copy(this.localBound); + } + } + + return this.selectionBoundStorage; + } + // get local space bound get localBound() { if (this.localBoundDirty) { @@ -499,7 +529,7 @@ class Splat extends Element { set visible(value: boolean) { if (value !== this.visible) { this.visible_ = value; - this.scene.events.fire('splat.vis', this); + this.scene.events.fire('splat.visibility', this); } } } diff --git a/src/tools/move-tool.ts b/src/tools/move-tool.ts index ca43294..234fd8c 100644 --- a/src/tools/move-tool.ts +++ b/src/tools/move-tool.ts @@ -5,10 +5,10 @@ import { EditHistory } from '../edit-history'; import { Scene } from '../scene'; class MoveTool extends TransformTool { - constructor(events: Events, editHistory: EditHistory, scene: Scene) { + constructor(events: Events, scene: Scene) { const gizmo = new TranslateGizmo(scene.app, scene.camera.entity.camera, scene.gizmoLayer); - super(gizmo, events, editHistory, scene); + super(gizmo, events, scene); } } diff --git a/src/tools/rotate-tool.ts b/src/tools/rotate-tool.ts index 9487f26..fc1eb04 100644 --- a/src/tools/rotate-tool.ts +++ b/src/tools/rotate-tool.ts @@ -5,10 +5,10 @@ import { EditHistory } from '../edit-history'; import { Scene } from '../scene'; class RotateTool extends TransformTool { - constructor(events: Events, editHistory: EditHistory, scene: Scene) { + constructor(events: Events, scene: Scene) { const gizmo = new RotateGizmo(scene.app, scene.camera.entity.camera, scene.gizmoLayer); - super(gizmo, events, editHistory, scene); + super(gizmo, events, scene); } } diff --git a/src/tools/scale-tool.ts b/src/tools/scale-tool.ts index 122a5f8..989f905 100644 --- a/src/tools/scale-tool.ts +++ b/src/tools/scale-tool.ts @@ -5,7 +5,7 @@ import { EditHistory } from '../edit-history'; import { Scene } from '../scene'; class ScaleTool extends TransformTool { - constructor(events: Events, editHistory: EditHistory, scene: Scene) { + constructor(events: Events, scene: Scene) { const gizmo = new ScaleGizmo(scene.app, scene.camera.entity.camera, scene.gizmoLayer); // disable everything except uniform scale @@ -13,7 +13,7 @@ class ScaleTool extends TransformTool { gizmo.enableShape(axis, false); }); - super(gizmo, events, editHistory, scene); + super(gizmo, events, scene); } } diff --git a/src/tools/transform-tool.ts b/src/tools/transform-tool.ts index 1fadd0e..bc5cce5 100644 --- a/src/tools/transform-tool.ts +++ b/src/tools/transform-tool.ts @@ -1,9 +1,8 @@ -import { TransformGizmo, Vec3 } from 'playcanvas'; +import { Entity, GraphicsDevice, TransformGizmo, Vec3 } from 'playcanvas'; import { Scene } from '../scene'; import { Splat } from '../splat'; import { Events } from '../events'; -import { EditHistory } from '../edit-history'; -import { EntityOp, EntityTransformOp } from '../edit-ops'; +import { EditOp, EntityTransformOp, SetPivotOp } from '../edit-ops'; // patch gizmo to be more opaque const patchGizmoMaterials = (gizmo: TransformGizmo) => { @@ -13,173 +12,145 @@ const patchGizmoMaterials = (gizmo: TransformGizmo) => { gizmo._meshColors.disabled.a = 0.8; }; -class SetPivotOp { - name = "setPivot"; - splat: Splat; - oldPivot: Vec3; - newPivot: Vec3; - - constructor(splat: Splat, oldPivot: Vec3, newPivot: Vec3) { - this.splat = splat; - this.oldPivot = oldPivot; - this.newPivot = newPivot; - } - - do() { - this.splat.setPivot(this.newPivot); +// set the gizmo size to remain a constant size in screen space. +// called in response to changes in canvas size +const updateGizmoSize = (gizmo: TransformGizmo, device: GraphicsDevice) => { + const canvas = document.getElementById('canvas'); + if (canvas) { + const w = canvas.clientWidth; + const h = canvas.clientHeight; + gizmo.size = 1200 / Math.max(w, h); + + // FIXME: + // this is a temporary workaround to undo gizmo's own auto scaling. + // once gizmo's autoscaling code is removed, this line can go too. + // @ts-ignore + gizmo._deviceStartSize = Math.min(device.width, device.height); } +}; - undo() { - this.splat.setPivot(this.oldPivot); - } -} +const copyTransform = (target: Entity, source: Entity) => { + target.setLocalPosition(source.getLocalPosition()); + target.setLocalRotation(source.getLocalRotation()); + target.setLocalScale(source.getLocalScale()); +}; class TransformTool { - scene: Scene; - gizmo: TransformGizmo; - splats: Splat[] = []; - ops: EntityOp[] = []; - events: Events; - active = false; - - constructor(gizmo: TransformGizmo, events: Events, editHistory: EditHistory, scene: Scene) { - this.scene = scene; - this.gizmo = gizmo; - this.events = events; - - // patch gizmo materials (until we have API to do this) - patchGizmoMaterials(this.gizmo); - - this.gizmo.coordSpace = events.invoke('tool.coordSpace'); - - this.gizmo.on('render:update', () => { - scene.forceRender = true; - }); - - this.gizmo.on('transform:start', () => { - this.ops = this.splats.map((splat) => { - const pivot = splat.pivot; - - return { - splat, - old: { - position: pivot.getLocalPosition().clone(), - rotation: pivot.getLocalRotation().clone(), - scale: pivot.getLocalScale().clone() - }, - new: { - position: pivot.getLocalPosition().clone(), - rotation: pivot.getLocalRotation().clone(), - scale: pivot.getLocalScale().clone() - } + activate: () => void; + deactivate: () => void; + + constructor(gizmo: TransformGizmo, events: Events, scene: Scene) { + let active = false; + let target: Splat = null; + let op: EntityTransformOp = null; + + // create the transform pivot + const pivot = new Entity('gizmoPivot'); + scene.app.root.addChild(pivot); + + gizmo.on('transform:start', () => { + const p = pivot.getLocalPosition().clone(); + const r = pivot.getLocalRotation().clone(); + const s = pivot.getLocalScale().clone(); + + // create a new op instance on start + op = new EntityTransformOp({ + splat: target, + oldt: { + position: p.clone(), + rotation: r.clone(), + scale: s.clone() + }, + newt: { + position: p.clone(), + rotation: r.clone(), + scale: s.clone() } }); }); - this.gizmo.on('transform:move', () => { - this.ops.forEach((op) => { - op.splat.worldBoundDirty = true; - }); + gizmo.on('render:update', () => { + scene.forceRender = true; + }); + + gizmo.on('transform:move', () => { + copyTransform(target.pivot, pivot); + target.worldBoundDirty = true; scene.boundDirty = true; }); - this.gizmo.on('transform:end', () => { + gizmo.on('transform:end', () => { + const p = pivot.getLocalPosition(); + const r = pivot.getLocalRotation(); + const s = pivot.getLocalScale(); + // update new transforms - this.ops.forEach((op) => { - const e = op.splat.pivot; - op.new.position.copy(e.getLocalPosition()); - op.new.rotation.copy(e.getLocalRotation()); - op.new.scale.copy(e.getLocalScale()); - }); + if (!p.equals(op.oldt.position) || + !r.equals(op.oldt.rotation) || + !s.equals(op.oldt.scale)) { - // filter out ops that didn't change - this.ops = this.ops.filter((op) => { - const e = op.splat.pivot; - return !op.old.position.equals(e.getLocalPosition()) || - !op.old.rotation.equals(e.getLocalRotation()) || - !op.old.scale.equals(e.getLocalScale()); - }); + op.newt.position.copy(p); + op.newt.rotation.copy(r); + op.newt.scale.copy(s); - if (this.ops.length > 0) { - editHistory.add(new EntityTransformOp(this.ops)); - this.ops = []; + events.fire('edit.add', op); } + + op = null; }); - events.on('scene.boundChanged', () => { - if (this.splats) { - this.gizmo.attach(this.splats.map((splat) => splat.pivot)); + // reattach the gizmo to the pivot + const reattach = () => { + const selection = events.invoke('selection') as Splat; + + if (!active || !selection) { + gizmo.detach(); + target = null; + } else { + target = selection; + copyTransform(pivot, target.pivot); + gizmo.attach([pivot]); } - }); + }; events.on('tool.coordSpace', (coordSpace: string) => { - this.gizmo.coordSpace = coordSpace; + gizmo.coordSpace = coordSpace; scene.forceRender = true; }); - events.on('selection.changed', () => { - this.update(); - }); - - events.on('splat.moved', (splat: Splat) => { - this.update(); - }); - - const updateGizmoSize = () => { - const canvas = document.getElementById('canvas'); - if (canvas) { - const w = canvas.clientWidth; - const h = canvas.clientHeight; - this.gizmo.size = 1200 / Math.max(w, h); - - // FIXME: - // this is a temporary workaround to undo gizmo's own auto scaling. - // once gizmo's autoscaling code is removed, this line can go too. - // @ts-ignore - this.gizmo._deviceStartSize = Math.min(scene.app.graphicsDevice.width, scene.app.graphicsDevice.height); - } - }; + events.on('scene.boundChanged', reattach); + events.on('selection.changed', reattach); + events.on('splat.moved', reattach); events.on('camera.resize', () => { - this.scene.events.on('camera.resize', updateGizmoSize); + scene.events.on('camera.resize', () => updateGizmoSize(gizmo, scene.graphicsDevice)); }); events.on('camera.focalPointPicked', (details: { splat: Splat, position: Vec3 }) => { - if (this.active) { + if (active) { const op = new SetPivotOp(details.splat, details.splat.pivot.getLocalPosition().clone(), details.position.clone()); - editHistory.add(op); + events.fire('edit.add', op); } }); - updateGizmoSize(); - } - - update() { - if (!this.active) { - this.gizmo.detach(); - this.splats = []; - return; + this.activate = () => { + active = true; + reattach(); } - const selection = this.events.invoke('selection') as Splat; - if (!selection) { - this.gizmo.detach(); - this.splats = []; - return; + this.deactivate = () => { + active = false; + reattach(); } - this.splats = [ selection ]; - this.gizmo.attach(this.splats.map((splats) => splats.pivot)); - } + // update gizmo size + updateGizmoSize(gizmo, scene.graphicsDevice); - activate() { - this.active = true; - this.update(); - } + // patch gizmo materials (until we have API to do this) + patchGizmoMaterials(gizmo); - deactivate() { - this.active = false; - this.update(); + // initialize coodinate space + gizmo.coordSpace = events.invoke('tool.coordSpace'); } } diff --git a/src/ui/data-panel.ts b/src/ui/data-panel.ts index ab4b660..f395c3a 100644 --- a/src/ui/data-panel.ts +++ b/src/ui/data-panel.ts @@ -2,7 +2,7 @@ import { BooleanInput, Container, Label, Panel, SelectInput } from 'pcui'; import { Events } from '../events'; import { Splat } from '../splat'; import { Histogram } from './histogram'; -import { State } from '../edit-ops'; +import { State } from '../splat-state'; import { localize } from './localization'; import { rgb2hsv } from './color'; @@ -242,24 +242,10 @@ class DataPanel extends Panel { const state = splat.splatData.getProp('state') as Uint8Array; if (state) { - // calculate totals - let selected = 0; - let hidden = 0; - let deleted = 0; - for (let i = 0; i < state.length; ++i) { - if (state[i] & State.deleted) { - deleted++; - } else if (state[i] & State.hidden) { - hidden++; - } else if (state[i] & State.selected) { - selected++; - } - } - - splatsValue.text = (state.length - deleted).toString(); - selectedValue.text = selected.toString(); - hiddenValue.text = hidden.toString(); - deletedValue.text = deleted.toString(); + splatsValue.text = (state.length - splat.numDeleted).toString(); + selectedValue.text = splat.numSelected.toString(); + hiddenValue.text = splat.numHidden.toString(); + deletedValue.text = splat.numDeleted.toString(); // update histogram const func = getValueFunc(); diff --git a/src/ui/splat-list.ts b/src/ui/splat-list.ts index cf93f6d..0ce8373 100644 --- a/src/ui/splat-list.ts +++ b/src/ui/splat-list.ts @@ -173,7 +173,7 @@ class SplatList extends Container { }); }); - events.on('splat.vis', (splat: Splat) => { + events.on('splat.visibility', (splat: Splat) => { const item = items.get(splat); if (item) { item.visible = splat.visible; diff --git a/src/ui/transform.ts b/src/ui/transform.ts index 65358e0..fd7a12a 100644 --- a/src/ui/transform.ts +++ b/src/ui/transform.ts @@ -158,23 +158,23 @@ class Transform extends Container { const r = selection.pivot.getLocalRotation(); const s = selection.pivot.getLocalScale(); - op = new EntityTransformOp([{ + op = new EntityTransformOp({ splat: selection, - old: { + oldt: { position: p.clone(), rotation: r.clone(), scale: s.clone() }, - new: { + newt: { position: p.clone(), rotation: r.clone(), scale: s.clone() } - }]); + }); }; const updateOp = () => { - const n = op.entityOps[0].new; + const n = op.newt; const p = positionVector.value; n.position.x = p[0];