-
-
Notifications
You must be signed in to change notification settings - Fork 94
fix(embed): make SET_CAMERA, RESET_COLORS and ENTITY_HOVERED actually work (#2934) #2978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
BIMvoice
wants to merge
4
commits into
main
Choose a base branch
from
fix-2934-inert-embed-commands
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
eb0c63e
fix(embed): make SET_CAMERA, RESET_COLORS and ENTITY_HOVERED do somet…
BIMvoice 54d1a09
Merge branch 'main' into fix-2934-inert-embed-commands
louistrue 7aa31ef
test(renderer): observe setRotation's up reset from a non-Y up pose
BIMvoice 57fec0f
docs(changeset): state RESET_COLORS' removed side effect, and its fed…
BIMvoice File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
52 changes: 52 additions & 0 deletions
52
.changeset/embed-set-camera-reset-colors-entity-hovered.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| --- | ||
| '@ifc-lite/renderer': minor | ||
| '@ifc-lite/embed-protocol': patch | ||
| '@ifc-lite/embed-sdk': patch | ||
| --- | ||
|
|
||
| Three embed API commands that reported success while doing nothing now work | ||
| (#2934). Each was broken at a different link in the chain. | ||
|
|
||
| `SET_CAMERA` had no actuator. The handler called the store's | ||
| `setCameraRotation`, which was `set({ cameraRotation })` and nothing more — | ||
| every orientation entry point on the camera was either relative (`orbit`, the | ||
| 90° rotate steppers) or named a direction (`setPresetView`), so an absolute | ||
| azimuth/elevation pair had nothing to reach. The host got a `requestId` ack | ||
| *and* a `CAMERA_CHANGED` echo of its own numbers back, while the view never | ||
| moved. `Camera.setRotation(azimuth, elevation)` is new on `@ifc-lite/renderer` | ||
| — the exact inverse of `Camera.getRotation`, absolute and idempotent, keeping | ||
| the target and orbit distance, with the same pole clamp `orbit` uses — and the | ||
| store action now drives it the way `setProjectionMode` drives its own callback. | ||
|
|
||
| `RESET_COLORS` cleared the wrong channel, in both directions at once. | ||
| `SET_COLORS` bakes into the mesh colors, while `clearPendingColorUpdates` | ||
| empties the transient overlay channel the lens, IDS, clash and schedule | ||
| overlays own: the host's own override survived the reset, and another | ||
| subsystem's state was destroyed by it. `SET_COLORS` now marks its writes as an | ||
| override, which captures the colors it displaces, and `RESET_COLORS` restores | ||
| those and leaves the overlay channel alone. The loader's own IFC style pass is | ||
| deliberately not treated as an override, so a reset restores the model's IFC | ||
| colors rather than stripping them. | ||
|
|
||
| For integrators, that second half is a behaviour change on a published surface | ||
| and not only a fix: `RESET_COLORS` no longer clears `pendingColorUpdates`. A | ||
| host that had been sending it to clear a lens, IDS, clash or schedule overlay | ||
| was relying on a side effect that is now gone, and must clear that overlay | ||
| through the command that owns it. `RESET_COLORS` only undoes `SET_COLORS`. | ||
|
|
||
| Also worth knowing before you rely on it: `RESET_COLORS` restores the entities | ||
| the viewer holds in its primary `geometryResult`, which is the FIRST loaded | ||
| model. In a federated embed with more than one model, `SET_COLORS` still | ||
| colours entities in the later models and `RESET_COLORS` does not restore them, | ||
| while both commands ack success. Single-model embeds — the common case — are | ||
| unaffected. | ||
|
|
||
| `ENTITY_HOVERED` was declared, exposed by the SDK, and never emitted — the SDK | ||
| tests passed because they fabricated the event themselves. The viewer's hover | ||
| pipeline was already there but gated behind a toolbar toggle the embed has no | ||
| chrome to offer; the embed now enables it and emits on each hover-target | ||
| change. | ||
|
|
||
| `SET_CAMERA`'s `zoom` field remains unapplied and is now documented as | ||
| reserved rather than silently dropped: it has no defined meaning on the viewer | ||
| side, and guessing one is worse than saying so. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| /** | ||
| * `SET_CAMERA` / `SET_COLORS` / `RESET_COLORS` against the REAL store slices. | ||
| * | ||
| * `handler.test.ts` drives a recording double of the store, which is the right | ||
| * shape for "did the bridge dispatch the right thing" but is structurally | ||
| * blind to the failure these three commands actually had (#2934): the handler | ||
| * called a real store action, the action existed, and the action did nothing. | ||
| * A double records the call and passes. | ||
| * | ||
| * So this file wires the handler to `createDataSlice` / `createCameraSlice` | ||
| * themselves and asserts the EFFECT — the mesh color that comes back, the | ||
| * orientation that reaches the camera actuator, the overlay channel that is | ||
| * still intact afterwards. | ||
| */ | ||
|
|
||
| import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| // Same narrow stand-in handler.test.ts uses: the bridge needs exactly one | ||
| // function from the store barrel, and importing the real barrel would drag in | ||
| // zustand + renderer + wasm. The slice creators below are imported directly, | ||
| // so the store logic under test is the real thing. | ||
| vi.mock('@/store/index.js', () => ({ | ||
| toGlobalIdFromModels: ( | ||
| _models: ReadonlyMap<string, { idOffset?: number }>, | ||
| _modelId: string, | ||
| expressId: number, | ||
| ): number => expressId, | ||
| })); | ||
|
|
||
| import { EMBED_SOURCE, PROTOCOL_VERSION } from '@ifc-lite/embed-protocol'; | ||
| import { createDataSlice } from '@/store/slices/dataSlice.js'; | ||
| import { createCameraSlice } from '@/store/slices/cameraSlice.js'; | ||
| import type { CameraRotation } from '@/store/types.js'; | ||
| import { initBridge, destroyBridge } from './handler.js'; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Window double (postMessage in, postMessage out) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| function installWindow() { | ||
| const listeners = new Set<(e: unknown) => void>(); | ||
| const win: any = { | ||
| addEventListener: (type: string, fn: (e: unknown) => void) => { | ||
| if (type === 'message') listeners.add(fn); | ||
| }, | ||
| removeEventListener: (type: string, fn: (e: unknown) => void) => { | ||
| if (type === 'message') listeners.delete(fn); | ||
| }, | ||
| }; | ||
| win.parent = { postMessage: () => { /* replies are not the subject here */ } }; | ||
| (globalThis as any).window = win; | ||
| return { | ||
| dispatch: (data: unknown) => { | ||
| for (const fn of [...listeners]) fn({ data, origin: 'https://host.example', source: win.parent }); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function cmd(type: string, data?: unknown) { | ||
| return { source: EMBED_SOURCE, version: PROTOCOL_VERSION, type, data, requestId: 'r1' }; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Real slices, composed the way the store composes them | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const mesh = (expressId: number, color: [number, number, number, number]) => ({ | ||
| expressId, | ||
| positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]), | ||
| indices: new Uint32Array([0, 1, 2]), | ||
| normals: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]), | ||
| color, | ||
| ifcType: 'IfcWall', | ||
| }); | ||
|
|
||
| function makeRealState() { | ||
| const rotations: CameraRotation[] = []; | ||
| let state: any; | ||
| const set = (partial: any) => { | ||
| const updates = typeof partial === 'function' ? partial(state) : partial; | ||
| state = { ...state, ...updates }; | ||
| }; | ||
| const get = () => state; | ||
|
|
||
| state = { | ||
| ...createDataSlice(set, get, undefined as never), | ||
| ...createCameraSlice(set, get, undefined as never), | ||
| activeModelId: null, | ||
| models: new Map(), | ||
| // Stand-in for the renderer-side actuator the Viewport registers | ||
| // (Viewport.tsx -> camera.setRotation). What it does with the angles is | ||
| // `packages/renderer/src/camera-absolute-rotation.test.ts`'s subject; what | ||
| // matters here is that the command reaches it at all. | ||
| cameraCallbacks: { | ||
| setCameraRotation: (rotation: CameraRotation) => { rotations.push(rotation); }, | ||
| }, | ||
| }; | ||
|
|
||
| return { | ||
| rotations, | ||
| getState: () => state, | ||
| }; | ||
| } | ||
|
|
||
| describe('bridge commands against the real store slices', () => { | ||
| let win: ReturnType<typeof installWindow>; | ||
| let store: ReturnType<typeof makeRealState>; | ||
|
|
||
| beforeEach(() => { | ||
| win = installWindow(); | ||
| store = makeRealState(); | ||
| initBridge({ | ||
| getState: store.getState as never, | ||
| loadModelFromUrl: vi.fn(), | ||
| loadModelFromBuffer: vi.fn(), | ||
| addModelFromUrl: vi.fn(), | ||
| } as never); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| destroyBridge(); | ||
| }); | ||
|
|
||
| describe('SET_CAMERA', () => { | ||
| it('reaches the camera actuator, not just the store field', () => { | ||
| // The whole defect: `setCameraRotation` wrote `cameraRotation` and | ||
| // stopped there, so the host got a success ack and a CAMERA_CHANGED echo | ||
| // of its own numbers while the view never moved. | ||
| win.dispatch(cmd('SET_CAMERA', { azimuth: 120, elevation: 30 })); | ||
|
|
||
| expect(store.rotations).toEqual([{ azimuth: 120, elevation: 30 }]); | ||
| }); | ||
|
|
||
| it('records the new orientation in the store as well', () => { | ||
| win.dispatch(cmd('SET_CAMERA', { azimuth: 120, elevation: 30 })); | ||
|
|
||
| expect(store.getState().cameraRotation).toEqual({ azimuth: 120, elevation: 30 }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('RESET_COLORS', () => { | ||
| it('actually restores the color SET_COLORS baked in', () => { | ||
| store.getState().appendGeometryBatch([mesh(12, [1, 0, 0, 1])] as never); | ||
|
|
||
| win.dispatch(cmd('SET_COLORS', { colorMap: { '12': [0, 1, 0, 1] } })); | ||
| expect(store.getState().geometryResult.meshes[0].color).toEqual([0, 1, 0, 1]); | ||
|
|
||
| win.dispatch(cmd('RESET_COLORS')); | ||
|
|
||
| expect(store.getState().geometryResult.meshes[0].color).toEqual([1, 0, 0, 1]); | ||
| // And the renderer is told to re-upload the restored color, otherwise the | ||
| // GPU keeps showing the override. | ||
| expect(store.getState().pendingMeshColorUpdates.get(12)).toEqual([1, 0, 0, 1]); | ||
| }); | ||
|
|
||
| it('leaves another subsystem\'s overlay colors intact', () => { | ||
| // `pendingColorUpdates` is the lens / IDS / clash / schedule overlay | ||
| // channel. RESET_COLORS used to clear exactly this and nothing else — | ||
| // wrong in both directions at once: the host's own override survived, | ||
| // and an overlay owner's state was destroyed. | ||
| store.getState().appendGeometryBatch([mesh(12, [1, 0, 0, 1])] as never); | ||
| store.getState().setPendingColorUpdates(new Map([[12, [1, 1, 0, 1]]])); | ||
|
|
||
| win.dispatch(cmd('SET_COLORS', { colorMap: { '12': [0, 1, 0, 1] } })); | ||
| win.dispatch(cmd('RESET_COLORS')); | ||
|
|
||
| expect(store.getState().pendingColorUpdates.get(12)).toEqual([1, 1, 0, 1]); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Add federation fixture coverage for hover metadata.
These tests only validate model-free IDs. They expect
globalIdandifcTypeto be undefined.Add tests with a real
FederationRegistryfixture. Test the single-modelglobalId === expressIdfallback. Test N models with overlapping express IDs. Assert the emittedglobalId,modelIdwhen supported by the event contract, andifcType.As per coding guidelines, “Resolve selections/IDs through
FederationRegistry” and “Verify behaviour atmodels.sizeof 1 and N.”🤖 Prompt for AI Agents
Source: Coding guidelines