@salsita-ai/sdk 0.49.0-develop.cbee856178 → 0.49.0-develop.d4bea2382c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application-spec/generate-application/code/project-scaffolding.js +2 -2
- package/dist/application-spec/generate-application/code/templates/index.d.ts +1 -1
- package/dist/application-spec/generate-application/code/templates/index.js +1 -1
- package/dist/application-spec/generate-application/code/templates/phoenix-feedback-skill.js +14 -2
- package/dist/application-spec/generate-application/code/templates/phoenix-skill.d.ts +13 -1
- package/dist/application-spec/generate-application/code/templates/phoenix-skill.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/camera-control-system/camera-control-system.d.ts +7 -3
- package/dist/configurator/application-runtime/extensions/systems/camera-control-system/camera-control-system.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/camera-control-system/camera-system.d.ts +24 -1
- package/dist/configurator/application-runtime/extensions/systems/camera-control-system/camera-system.js +1 -1
- package/dist/docs/3d-scene/cameras/custom.mdx +4 -1
- package/dist/docs/3d-scene/cameras/fit-entity.mdx +2 -0
- package/dist/docs/3d-scene/cameras/fit-root.mdx +2 -0
- package/dist/docs/3d-scene/cameras/fit-selection.mdx +2 -0
- package/dist/docs/3d-scene/cameras/locks.mdx +439 -0
- package/dist/docs/3d-scene/cameras/static.mdx +2 -0
- package/dist/docs/3d-scene/cameras/transitions.mdx +2 -0
- package/dist/docs/index.json +12 -1
- package/dist/docs/phoenix/phoenix-cli.mdx +1 -1
- package/dist/docs/phoenix/project-structure.mdx +1 -1
- package/dist/docs/phoenix/sdk-builder-workflow.mdx +4 -3
- package/dist/domain/projections/common/camera-orbit.d.ts +37 -9
- package/dist/domain/projections/common/camera-orbit.js +1 -1
- package/dist/domain/projections/common/index.d.ts +1 -1
- package/dist/domain/projections/common/index.js +1 -1
- package/dist/domain/system-components/fit-entity-orthographic-camera-component.d.ts +8 -0
- package/dist/domain/system-components/fit-entity-orthographic-camera-component.js +1 -1
- package/dist/domain/system-components/fit-entity-perspective-camera-component.d.ts +8 -0
- package/dist/domain/system-components/fit-entity-perspective-camera-component.js +1 -1
- package/dist/domain/system-components/fit-root-orthographic-camera-component.d.ts +8 -0
- package/dist/domain/system-components/fit-root-orthographic-camera-component.js +1 -1
- package/dist/domain/system-components/fit-root-perspective-camera-component.d.ts +8 -0
- package/dist/domain/system-components/fit-root-perspective-camera-component.js +1 -1
- package/dist/domain/system-components/fit-selection-orthographic-camera-component.d.ts +8 -0
- package/dist/domain/system-components/fit-selection-orthographic-camera-component.js +1 -1
- package/dist/domain/system-components/fit-selection-perspective-camera-component.d.ts +8 -0
- package/dist/domain/system-components/fit-selection-perspective-camera-component.js +1 -1
- package/dist/domain/system-components/static-orthographic-camera-component.d.ts +8 -0
- package/dist/domain/system-components/static-orthographic-camera-component.js +1 -1
- package/dist/domain/system-components/static-perspective-camera-component.d.ts +8 -0
- package/dist/domain/system-components/static-perspective-camera-component.js +1 -1
- package/dist/legacy/domain-spec/__generated/domain-schema.d.ts +16 -0
- package/package.json +3 -3
- package/schema/domain-spec.json +208 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# Camera - Locks
|
|
2
|
+
|
|
3
|
+
Keep a user's chosen viewing angle or magnification while the configurator moves between related product views. Camera locks place cameras into named groups: give two cameras the same explicit group and the destination camera retains that part of the live view when it activates.
|
|
4
|
+
|
|
5
|
+
- [Apply camera locks](#apply-camera-locks)
|
|
6
|
+
- [Choose what to preserve](#choose-what-to-preserve)
|
|
7
|
+
- [Understand transition behavior](#understand-transition-behavior)
|
|
8
|
+
- [Try the example](#try-the-example)
|
|
9
|
+
- [Example](#example)
|
|
10
|
+
- [Complete Implementation](#complete-implementation)
|
|
11
|
+
|
|
12
|
+
## Apply camera locks
|
|
13
|
+
|
|
14
|
+
Assign the same literal group name to the outgoing and incoming cameras that should share a live view. For perspective cameras, `distanceLockGroup` preserves dolly distance and `rotationLockGroup` preserves the live azimuth and polar angles:
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
components:
|
|
18
|
+
Scene:
|
|
19
|
+
fitRootPerspectiveCamera:
|
|
20
|
+
overview:
|
|
21
|
+
distanceLockGroup: product-view
|
|
22
|
+
rotationLockGroup: product-view
|
|
23
|
+
Product:
|
|
24
|
+
fitEntityPerspectiveCamera:
|
|
25
|
+
detail:
|
|
26
|
+
distanceLockGroup: product-view
|
|
27
|
+
rotationLockGroup: product-view
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
When the Product camera activates after the Scene camera, it still calculates its normal target from Product, but it uses the user's current distance and rotation. The same applies in the other direction. Group names have no predefined meaning; choose stable names that describe the related camera views in your configurator.
|
|
31
|
+
|
|
32
|
+
Orthographic cameras use `zoomLockGroup` instead of `distanceLockGroup`:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
components:
|
|
36
|
+
Scene:
|
|
37
|
+
fitRootOrthographicCamera:
|
|
38
|
+
overview:
|
|
39
|
+
zoomLockGroup: product-view
|
|
40
|
+
rotationLockGroup: product-view
|
|
41
|
+
Product:
|
|
42
|
+
fitEntityOrthographicCamera:
|
|
43
|
+
detail:
|
|
44
|
+
zoomLockGroup: product-view
|
|
45
|
+
rotationLockGroup: product-view
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Add the group property to every camera that participates. Group names match exactly and case-sensitively, including whitespace. A missing group, `null`, or a different string means that aspect follows the destination camera normally; two missing or `null` groups never create a lock.
|
|
49
|
+
|
|
50
|
+
## Choose what to preserve
|
|
51
|
+
|
|
52
|
+
Distance or zoom and rotation are independent. Configure only the behavior the product flow needs:
|
|
53
|
+
|
|
54
|
+
- Set only `rotationLockGroup` to retain the viewing angle while the destination camera calculates its normal fit distance or zoom.
|
|
55
|
+
- Set only `distanceLockGroup` on perspective cameras to retain dolly distance while applying the destination orientation.
|
|
56
|
+
- Set only `zoomLockGroup` on orthographic cameras to retain zoom while applying the destination orientation.
|
|
57
|
+
- Set both relevant groups to translate the retained view around the destination camera's newly calculated target.
|
|
58
|
+
|
|
59
|
+
Lock groups are available on fit-root, fit-entity, fit-selection, and static cameras. A camera can belong to different groups for rotation and magnification by using different names for the two aspects.
|
|
60
|
+
|
|
61
|
+
## Understand transition behavior
|
|
62
|
+
|
|
63
|
+
The destination camera still owns its target, boundary behavior, and limits. Preserved distance and zoom are clamped to its configured limits. Preserved angles use the same limit resolution as a normally applied camera: finite two-sided ranges clamp during the transition, while a one-sided azimuth limit is placed on its allowed side within one revolution of the preserved angle, so the angle stays where it is and the limit is reached by orbiting toward it. This can intentionally crop a newly fitted target: a lock preserves the user's view rather than guaranteeing that all destination content remains visible.
|
|
64
|
+
|
|
65
|
+
A matching group also applies when the active camera refits itself after mesh, bounds, or canvas-padding changes. This is useful with `fitMeshChanges`: geometry can change the target or fit while the chosen live aspects remain stable. A refit only preserves an aspect whose live value is a settled or user-chosen view: when it interrupts an unfinished transition that was still animating that aspect toward its destination, such as the first load, a Reset, or a switch from a camera with different groups, the refit completes the destination view instead of freezing the animation midway.
|
|
66
|
+
|
|
67
|
+
Locks apply only between cameras of the same projection type. Switching between perspective and orthographic cameras uses the destination view normally, even when their group names match. Projection parameters such as perspective `fov` and orthographic `frustumSize` are not preserved; keep them consistent when a visual projection snap would be distracting.
|
|
68
|
+
|
|
69
|
+
Reset Camera bypasses all locks for that action and restores the active camera's configured view. The next automatic transition uses locks normally again. Export rendering remains independent from the interactive camera's lock history.
|
|
70
|
+
|
|
71
|
+
## Try the example
|
|
72
|
+
|
|
73
|
+
The example uses dynamic expressions and sidebar switches only to make each lock easy to turn on and off while experimenting. A normal integration should use stable literal group names as shown above. Object B and the full-scene camera share a group, while Object A uses a different group:
|
|
74
|
+
|
|
75
|
+
- Object A: a fit-selection camera with the `object-a` group.
|
|
76
|
+
- Object B: a fit-entity camera activated by `$filter.selectedEntityIds`, with the `object-b-and-root` group.
|
|
77
|
+
- Nothing selected: a fit-root camera with the `object-b-and-root` group.
|
|
78
|
+
|
|
79
|
+
1. Leave both lock switches on, click Object B to select it, then orbit and zoom or dolly to an obvious view.
|
|
80
|
+
2. Click the empty canvas to clear the selection. The target changes to the whole scene, but the live rotation and magnification stay because B and the root share both groups.
|
|
81
|
+
3. Turn off one lock and repeat B → Nothing selected. The enabled aspect stays; the disabled aspect follows the destination camera.
|
|
82
|
+
4. Click Object A to select it. Its groups are different, so the configured A view is applied normally.
|
|
83
|
+
5. While B or Nothing selected is active, toggle **Show B extra part**. `fitMeshChanges` refits the same camera from mesh-derived bounds. An unlocked distance or zoom fits the larger shape while a locked rotation stays unchanged.
|
|
84
|
+
6. With both locks enabled, switch Perspective ↔ Orthographic. Projection changes never preserve the live view.
|
|
85
|
+
7. Change the live view and press the camera button in the toolbar. Reset Camera bypasses the locks once and restores the active camera's configured fit and angles.
|
|
86
|
+
8. In the perspective view with nothing selected, dolly fully out to the root camera's `maxDistance` of 9000, then click Object B. The preserved distance is clamped to B's `maxDistance` of 6500, so the camera moves closer while the rotation stays. In the orthographic view, zoom fully in on Object B and clear the selection: B's `maxZoom` of 3.5 is clamped to the root camera's 3.
|
|
87
|
+
|
|
88
|
+
For the camera activation rules used here, see [Fit Selection](?path=/docs/3d-scene-cameras-fit-selection--docs), [Fit Entity](?path=/docs/3d-scene-cameras-fit-entity--docs), [Fit Root](?path=/docs/3d-scene-cameras-fit-root--docs), and [Camera Transitions](?path=/docs/3d-scene-cameras-transitions--docs).
|
|
89
|
+
|
|
90
|
+
## Example
|
|
91
|
+
|
|
92
|
+
Use the sidebar to toggle each lock independently and change B's fitted mesh, the control over the canvas to switch projection, and the camera toolbar button to reset.
|
|
93
|
+
|
|
94
|
+
## Complete Implementation
|
|
95
|
+
|
|
96
|
+
{/* AUTO-GENERATED SECTION: This section is automatically updated by 'phoenix story build' */}
|
|
97
|
+
|
|
98
|
+
<details>
|
|
99
|
+
<summary>Click to view the complete implementation files</summary>
|
|
100
|
+
|
|
101
|
+
### ./spec/domain.yaml
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
model:
|
|
105
|
+
latest:
|
|
106
|
+
entityTypes:
|
|
107
|
+
- Scene
|
|
108
|
+
- ObjectA
|
|
109
|
+
- ObjectB
|
|
110
|
+
aliases:
|
|
111
|
+
RootEntity:
|
|
112
|
+
- Scene
|
|
113
|
+
shapes:
|
|
114
|
+
Scene:
|
|
115
|
+
lockRotation:
|
|
116
|
+
type: boolean
|
|
117
|
+
lockZoomOrDistance:
|
|
118
|
+
type: boolean
|
|
119
|
+
showBExtraPart:
|
|
120
|
+
type: boolean
|
|
121
|
+
|
|
122
|
+
relations:
|
|
123
|
+
Scene:
|
|
124
|
+
$objectA:
|
|
125
|
+
kind: allOfType
|
|
126
|
+
target: ObjectA
|
|
127
|
+
cardinality: 1-1
|
|
128
|
+
composedOf: true
|
|
129
|
+
$objectB:
|
|
130
|
+
kind: allOfType
|
|
131
|
+
target: ObjectB
|
|
132
|
+
cardinality: 1-1
|
|
133
|
+
composedOf: true
|
|
134
|
+
ObjectA:
|
|
135
|
+
$scene:
|
|
136
|
+
kind: allOfType
|
|
137
|
+
target: Scene
|
|
138
|
+
cardinality: 1-1
|
|
139
|
+
composedOf: false
|
|
140
|
+
ObjectB:
|
|
141
|
+
$scene:
|
|
142
|
+
kind: allOfType
|
|
143
|
+
target: Scene
|
|
144
|
+
cardinality: 1-1
|
|
145
|
+
composedOf: false
|
|
146
|
+
|
|
147
|
+
presets:
|
|
148
|
+
- name: Camera locks
|
|
149
|
+
description: Compare independent rotation and zoom or distance locks while changing camera targets.
|
|
150
|
+
rootEntityType: Scene
|
|
151
|
+
initialDomainEntities:
|
|
152
|
+
- id: Scene-1
|
|
153
|
+
type: Scene
|
|
154
|
+
lockRotation: true
|
|
155
|
+
lockZoomOrDistance: true
|
|
156
|
+
showBExtraPart: false
|
|
157
|
+
- id: ObjectA-1
|
|
158
|
+
type: ObjectA
|
|
159
|
+
- id: ObjectB-1
|
|
160
|
+
type: ObjectB
|
|
161
|
+
|
|
162
|
+
viewStates:
|
|
163
|
+
- perspective
|
|
164
|
+
- orthographic
|
|
165
|
+
|
|
166
|
+
# This example resolves lock groups from boolean attributes only so readers can
|
|
167
|
+
# compare locked and unlocked behavior interactively. In an application, assign
|
|
168
|
+
# stable literal lock groups directly to the related camera configurations.
|
|
169
|
+
components:
|
|
170
|
+
Scene:
|
|
171
|
+
applicationConfig:
|
|
172
|
+
main:
|
|
173
|
+
layout: universal
|
|
174
|
+
ignoreSelectionInSidebar: true
|
|
175
|
+
miniSidebar: true
|
|
176
|
+
splitSidebar: true
|
|
177
|
+
distanceDataUnit: mm
|
|
178
|
+
toolbar:
|
|
179
|
+
main:
|
|
180
|
+
desktop:
|
|
181
|
+
- kind: toolbarButtonGroup
|
|
182
|
+
children:
|
|
183
|
+
- kind: resetCameraToolbarButton
|
|
184
|
+
props:
|
|
185
|
+
icon: CameraRotate
|
|
186
|
+
mobile:
|
|
187
|
+
- kind: toolbarButtonGroup
|
|
188
|
+
children:
|
|
189
|
+
- kind: resetCameraToolbarButton
|
|
190
|
+
viewControls:
|
|
191
|
+
projection:
|
|
192
|
+
items:
|
|
193
|
+
- label: Perspective
|
|
194
|
+
value: perspective
|
|
195
|
+
icon: Eye
|
|
196
|
+
- label: Orthographic
|
|
197
|
+
value: orthographic
|
|
198
|
+
icon: Square
|
|
199
|
+
fitRootPerspectiveCamera:
|
|
200
|
+
nothingSelected:
|
|
201
|
+
near: 10
|
|
202
|
+
far: 20000
|
|
203
|
+
fov: 42
|
|
204
|
+
azimuthAngle: 0.55
|
|
205
|
+
polarAngle: 1.05
|
|
206
|
+
minDistance: 1200
|
|
207
|
+
maxDistance: 9000
|
|
208
|
+
distanceLockGroup:
|
|
209
|
+
$read: $distanceLockGroup
|
|
210
|
+
rotationLockGroup:
|
|
211
|
+
$read: $rotationLockGroup
|
|
212
|
+
fitMeshChanges: true
|
|
213
|
+
$filter:
|
|
214
|
+
viewStates: [perspective]
|
|
215
|
+
fitRootOrthographicCamera:
|
|
216
|
+
nothingSelected:
|
|
217
|
+
near: 10
|
|
218
|
+
far: 20000
|
|
219
|
+
frustumSize: 5000
|
|
220
|
+
distance: 6000
|
|
221
|
+
azimuthAngle: 0.55
|
|
222
|
+
polarAngle: 1.05
|
|
223
|
+
minZoom: 0.4
|
|
224
|
+
maxZoom: 3
|
|
225
|
+
zoomLockGroup:
|
|
226
|
+
$read: $distanceLockGroup
|
|
227
|
+
rotationLockGroup:
|
|
228
|
+
$read: $rotationLockGroup
|
|
229
|
+
fitMeshChanges: true
|
|
230
|
+
$filter:
|
|
231
|
+
viewStates: [orthographic]
|
|
232
|
+
attributeSwitch:
|
|
233
|
+
lockRotation:
|
|
234
|
+
value:
|
|
235
|
+
$bind: lockRotation
|
|
236
|
+
lockZoomOrDistance:
|
|
237
|
+
value:
|
|
238
|
+
$bind: lockZoomOrDistance
|
|
239
|
+
showBExtraPart:
|
|
240
|
+
value:
|
|
241
|
+
$bind: showBExtraPart
|
|
242
|
+
|
|
243
|
+
ObjectA:
|
|
244
|
+
fitSelectionPerspectiveCamera:
|
|
245
|
+
selected:
|
|
246
|
+
priority: 10
|
|
247
|
+
near: 10
|
|
248
|
+
far: 20000
|
|
249
|
+
fov: 42
|
|
250
|
+
azimuthAngle: -0.85
|
|
251
|
+
polarAngle: 1.25
|
|
252
|
+
minDistance: 700
|
|
253
|
+
maxDistance: 5000
|
|
254
|
+
distanceLockGroup:
|
|
255
|
+
$read: $distanceLockGroup
|
|
256
|
+
rotationLockGroup:
|
|
257
|
+
$read: $rotationLockGroup
|
|
258
|
+
$filter:
|
|
259
|
+
viewStates: [perspective]
|
|
260
|
+
fitSelectionOrthographicCamera:
|
|
261
|
+
selected:
|
|
262
|
+
priority: 10
|
|
263
|
+
near: 10
|
|
264
|
+
far: 20000
|
|
265
|
+
frustumSize: 5000
|
|
266
|
+
distance: 4000
|
|
267
|
+
azimuthAngle: -0.85
|
|
268
|
+
polarAngle: 1.25
|
|
269
|
+
minZoom: 0.5
|
|
270
|
+
maxZoom: 4
|
|
271
|
+
zoomLockGroup:
|
|
272
|
+
$read: $distanceLockGroup
|
|
273
|
+
rotationLockGroup:
|
|
274
|
+
$read: $rotationLockGroup
|
|
275
|
+
$filter:
|
|
276
|
+
viewStates: [orthographic]
|
|
277
|
+
sceneEntityBoxObject:
|
|
278
|
+
body:
|
|
279
|
+
width: 900
|
|
280
|
+
height: 1200
|
|
281
|
+
depth: 700
|
|
282
|
+
anchor: center
|
|
283
|
+
sceneEntityProperties:
|
|
284
|
+
main:
|
|
285
|
+
offsetX: -1600
|
|
286
|
+
offsetY: 600
|
|
287
|
+
sceneEntityLocalMaterialOverride:
|
|
288
|
+
main:
|
|
289
|
+
materialsToOverride: [BoxMaterial]
|
|
290
|
+
color: '#4f7cac'
|
|
291
|
+
roughness: 0.65
|
|
292
|
+
selectable:
|
|
293
|
+
main: {}
|
|
294
|
+
|
|
295
|
+
ObjectB:
|
|
296
|
+
fitEntityPerspectiveCamera:
|
|
297
|
+
selected:
|
|
298
|
+
priority: 10
|
|
299
|
+
near: 10
|
|
300
|
+
far: 20000
|
|
301
|
+
# Projection settings are intentionally identical to the root camera.
|
|
302
|
+
# Distance/zoom locks preserve controls state, not projection parameters;
|
|
303
|
+
# changing these would cause an immediate visual snap before the pan.
|
|
304
|
+
fov: 42
|
|
305
|
+
azimuthAngle: 1.0
|
|
306
|
+
polarAngle: 0.9
|
|
307
|
+
minDistance: 900
|
|
308
|
+
maxDistance: 6500
|
|
309
|
+
distanceLockGroup:
|
|
310
|
+
$read: $distanceLockGroup
|
|
311
|
+
rotationLockGroup:
|
|
312
|
+
$read: $rotationLockGroup
|
|
313
|
+
fitMeshChanges: true
|
|
314
|
+
$filter:
|
|
315
|
+
viewStates: [perspective]
|
|
316
|
+
selectedEntityIds:
|
|
317
|
+
$read: id
|
|
318
|
+
fitEntityOrthographicCamera:
|
|
319
|
+
selected:
|
|
320
|
+
priority: 10
|
|
321
|
+
near: 10
|
|
322
|
+
far: 20000
|
|
323
|
+
frustumSize: 5000
|
|
324
|
+
distance: 4500
|
|
325
|
+
azimuthAngle: 1.0
|
|
326
|
+
polarAngle: 0.9
|
|
327
|
+
minZoom: 0.4
|
|
328
|
+
maxZoom: 3.5
|
|
329
|
+
zoomLockGroup:
|
|
330
|
+
$read: $distanceLockGroup
|
|
331
|
+
rotationLockGroup:
|
|
332
|
+
$read: $rotationLockGroup
|
|
333
|
+
fitMeshChanges: true
|
|
334
|
+
$filter:
|
|
335
|
+
viewStates: [orthographic]
|
|
336
|
+
selectedEntityIds:
|
|
337
|
+
$read: id
|
|
338
|
+
sceneEntityBoxObject:
|
|
339
|
+
body:
|
|
340
|
+
width: 1100
|
|
341
|
+
height: 900
|
|
342
|
+
depth: 850
|
|
343
|
+
anchor: center
|
|
344
|
+
extra:
|
|
345
|
+
width: 1900
|
|
346
|
+
height: 350
|
|
347
|
+
depth: 500
|
|
348
|
+
anchor: center
|
|
349
|
+
$filter:
|
|
350
|
+
active:
|
|
351
|
+
$read: $showExtraPart
|
|
352
|
+
sceneEntityObjectProperties:
|
|
353
|
+
extra:
|
|
354
|
+
offsetX: 800
|
|
355
|
+
offsetY: 750
|
|
356
|
+
offsetZ: -250
|
|
357
|
+
$filter:
|
|
358
|
+
active:
|
|
359
|
+
$read: $showExtraPart
|
|
360
|
+
sceneEntityProperties:
|
|
361
|
+
main:
|
|
362
|
+
offsetX: 1600
|
|
363
|
+
offsetY: 450
|
|
364
|
+
sceneEntityLocalMaterialOverride:
|
|
365
|
+
main:
|
|
366
|
+
materialsToOverride: [BoxMaterial]
|
|
367
|
+
color: '#d9822b'
|
|
368
|
+
roughness: 0.65
|
|
369
|
+
selectable:
|
|
370
|
+
main: {}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### ./src/components/CameraLockControls.tsx
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
import { propertyPageComponent } from '@salsita-ai/sdk/domain'
|
|
377
|
+
import { AttributeSwitchSelector } from '@salsita-ai/sdk/react'
|
|
378
|
+
import { PropertyContainer } from '@salsita-ai/sdk/ui'
|
|
379
|
+
|
|
380
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
381
|
+
|
|
382
|
+
export default [
|
|
383
|
+
phx.domain.attachComponent
|
|
384
|
+
.withComponent(propertyPageComponent, 'main')
|
|
385
|
+
.withEntity('Scene')
|
|
386
|
+
.attach(s => ({
|
|
387
|
+
page: (
|
|
388
|
+
<PropertyContainer.Group spacing="large">
|
|
389
|
+
<PropertyContainer.Item
|
|
390
|
+
title="Try the camera locks"
|
|
391
|
+
description="Object B and the full-scene camera share lock groups, while Object A uses different groups. Orbit and zoom or dolly on Object B, then switch between camera targets to compare what is preserved. Disable either lock to make that part of the view follow the destination camera instead."
|
|
392
|
+
/>
|
|
393
|
+
<AttributeSwitchSelector
|
|
394
|
+
entityId={s.id}
|
|
395
|
+
instanceName="lockRotation"
|
|
396
|
+
title="Lock rotation"
|
|
397
|
+
description="Preserve live azimuth and polar angles."
|
|
398
|
+
/>
|
|
399
|
+
<AttributeSwitchSelector
|
|
400
|
+
entityId={s.id}
|
|
401
|
+
instanceName="lockZoomOrDistance"
|
|
402
|
+
title="Lock zoom / distance"
|
|
403
|
+
description="Preserve perspective distance or orthographic zoom."
|
|
404
|
+
/>
|
|
405
|
+
<AttributeSwitchSelector
|
|
406
|
+
entityId={s.id}
|
|
407
|
+
instanceName="showBExtraPart"
|
|
408
|
+
title="Show B extra part"
|
|
409
|
+
description="Change B's mesh bounds to trigger a same-camera refit."
|
|
410
|
+
/>
|
|
411
|
+
</PropertyContainer.Group>
|
|
412
|
+
),
|
|
413
|
+
})),
|
|
414
|
+
]
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### ./src/expressions/camera-locks.ts
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
421
|
+
|
|
422
|
+
export default phx.domain.implementCustomExpressions({
|
|
423
|
+
Scene: {
|
|
424
|
+
$distanceLockGroup: s => (s.lockZoomOrDistance ? 'object-b-and-root' : null),
|
|
425
|
+
$rotationLockGroup: s => (s.lockRotation ? 'object-b-and-root' : null),
|
|
426
|
+
},
|
|
427
|
+
ObjectA: {
|
|
428
|
+
$distanceLockGroup: s => (s.$scene.lockZoomOrDistance ? 'object-a' : null),
|
|
429
|
+
$rotationLockGroup: s => (s.$scene.lockRotation ? 'object-a' : null),
|
|
430
|
+
},
|
|
431
|
+
ObjectB: {
|
|
432
|
+
$distanceLockGroup: s => (s.$scene.lockZoomOrDistance ? 'object-b-and-root' : null),
|
|
433
|
+
$rotationLockGroup: s => (s.$scene.lockRotation ? 'object-b-and-root' : null),
|
|
434
|
+
$showExtraPart: s => s.$scene.showBExtraPart,
|
|
435
|
+
},
|
|
436
|
+
})
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
</details>
|
|
@@ -541,6 +541,8 @@ Choose perspective cameras for natural, immersive viewing experiences. Choose or
|
|
|
541
541
|
|
|
542
542
|
## Example
|
|
543
543
|
|
|
544
|
+
To retain the user's live rotation, distance, or zoom when switching between static camera views that share a lock group, see the [Camera Locks example](?path=/docs/3d-scene-cameras-locks--docs).
|
|
545
|
+
|
|
544
546
|
This example shows multiple static cameras combining both perspective and orthographic projections positioned to showcase the model from different specific angles. Each camera maintains its configured position while still allowing user interaction within defined limits.
|
|
545
547
|
|
|
546
548
|
Try using the view controls at the top of the interface to switch between different camera perspectives. Notice how the 3D and Front views provide natural perspective viewing with depth, while the Top and Left views use true orthographic projection with `frustumSize` for technical accuracy. The orthographic views use `animateTransition: false` to provide instant, precise positioning ideal for technical examination.
|
|
@@ -117,6 +117,8 @@ The camera system uses memoization to maintain the same component instance when
|
|
|
117
117
|
|
|
118
118
|
This example combines all three transition scenarios. Use the step navigation to see component instance changes, adjust the radius slider in "Bounding Box" view to see geometry-based transitions, and toggle the box visibility in "Fit Mesh" view to see mesh-change transitions.
|
|
119
119
|
|
|
120
|
+
To preserve a user's live rotation, distance, or zoom across selected camera transitions, see the [Camera Locks example](?path=/docs/3d-scene-cameras-locks--docs).
|
|
121
|
+
|
|
120
122
|
## Complete Implementation
|
|
121
123
|
|
|
122
124
|
{/* AUTO-GENERATED SECTION: This section is automatically updated by 'phoenix story build' */}
|
package/dist/docs/index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-10-
|
|
2
|
+
"generatedAt": "2026-10-07T13:21:27.057Z",
|
|
3
3
|
"categories": [
|
|
4
4
|
{
|
|
5
5
|
"name": "Phoenix",
|
|
@@ -1381,6 +1381,17 @@
|
|
|
1381
1381
|
],
|
|
1382
1382
|
"source": "storybook-example"
|
|
1383
1383
|
},
|
|
1384
|
+
{
|
|
1385
|
+
"title": "Locks",
|
|
1386
|
+
"description": "Keep a user's chosen viewing angle or magnification while the configurator moves between related product views. Camera locks place cameras into named groups: give two cameras the same explicit group...",
|
|
1387
|
+
"path": "docs/3d-scene/cameras/locks.mdx",
|
|
1388
|
+
"category": [
|
|
1389
|
+
"3d scene",
|
|
1390
|
+
"Cameras",
|
|
1391
|
+
"Locks"
|
|
1392
|
+
],
|
|
1393
|
+
"source": "storybook-example"
|
|
1394
|
+
},
|
|
1384
1395
|
{
|
|
1385
1396
|
"title": "Simple",
|
|
1386
1397
|
"description": "This example demonstrates the default camera behavior in Phoenix configurators. When no camera components are configured, the system provides built-in camera controls with sensible defaults, giving...",
|
|
@@ -17,7 +17,7 @@ The Phoenix CLI (`phoenix` command) is the unified tool for all configurator dev
|
|
|
17
17
|
`phoenix init [folder]` starts a configurator project in `[folder]` (default: the working directory):
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npx -p @salsita-ai/sdk phoenix init my-configurator
|
|
20
|
+
npx -p @salsita-ai/sdk@canary phoenix init my-configurator
|
|
21
21
|
cd my-configurator
|
|
22
22
|
pnpm install
|
|
23
23
|
pnpm phoenix sync
|
|
@@ -197,7 +197,7 @@ type CustomMaterial = Phx['AdminModel']['CustomMaterial']
|
|
|
197
197
|
|
|
198
198
|
### Agent Guidance
|
|
199
199
|
|
|
200
|
-
Coding agents (Claude Code, Codex, Cursor) read `AGENTS.md` and the Phoenix agent skills. `AGENTS.md` is written once with the build commands, the do-not-edit list and a pointer to the skills; after that it is yours. The `phoenix`, `phoenix-cli`, `phoenix-feedback` and `phoenix-review` skills carry the SDK conventions, CLI tooling and review checklist, and the `phoenix-create-configurator` skill walks the agent from your product documents and assets to a reviewed configurator in five stages. Phoenix keeps them in step with the installed SDK, including the reference files inside a skill folder. Other skill folders are never touched.
|
|
200
|
+
Coding agents (Claude Code, Codex, Cursor) read `AGENTS.md` and the Phoenix agent skills. `AGENTS.md` is written once with the build commands, the do-not-edit list and a pointer to the skills; after that it is yours. The `phoenix`, `phoenix-cli`, `phoenix-feedback` and `phoenix-review` skills carry the SDK conventions, CLI tooling and review checklist, and the `phoenix-create-configurator` skill walks the agent from your product documents and assets to a reviewed configurator in five stages. Phoenix keeps them in step with the installed SDK, including the reference files inside a skill folder. During the internal beta, a project created from a canary build of the SDK gets one more section in the `phoenix` skill: once per conversation the agent checks for a newer canary and, after telling you, updates the project's SDK and runs `phoenix sync`. Other skill folders are never touched.
|
|
201
201
|
|
|
202
202
|
Projects created by earlier SDK versions had this guidance in `CLAUDE.md`, `.cursor/rules/phoenix.mdc` and `commands/review.md`. Sync removes the SDK block from the first two, keeps your own text around it, deletes a file that held nothing else, and deletes the review commands; use the `phoenix-review` skill instead of `/review`.
|
|
203
203
|
|
|
@@ -4,18 +4,19 @@ An SDK-builder workspace lets you build one configurator from the application sp
|
|
|
4
4
|
|
|
5
5
|
Your workspace comes with one project and one `production` environment. Its first deployment is a hello-world configurator, so you have a live link before you change anything.
|
|
6
6
|
|
|
7
|
-
Your workspace's Studio shows the setup, the link to your configurator and the setup steps with your workspace filled in.
|
|
7
|
+
Your workspace's Studio shows the setup, the link to your configurator and the setup steps with your workspace filled in. If the first deployment fails, Studio shows why and lets you retry it.
|
|
8
8
|
|
|
9
9
|
## Before you start
|
|
10
10
|
|
|
11
11
|
- **Package access:** `@salsita-ai/sdk` is public on npm; `npx` and `pnpm install` download it without a registry token.
|
|
12
12
|
- **Workspace details:** you need your workspace slug and its Phoenix region (`eu` or `us`), which you also received with your workspace.
|
|
13
13
|
- **Tools:** Node.js and pnpm.
|
|
14
|
+
- **SDK build:** during the internal beta, use the `canary` build of the SDK, as the commands below do. It is published from every change to the platform, so it is newer than the last release.
|
|
14
15
|
|
|
15
16
|
## 1. Create a project
|
|
16
17
|
|
|
17
18
|
```bash
|
|
18
|
-
npx -p @salsita-ai/sdk phoenix init my-configurator
|
|
19
|
+
npx -p @salsita-ai/sdk@canary phoenix init my-configurator
|
|
19
20
|
cd my-configurator
|
|
20
21
|
pnpm install
|
|
21
22
|
pnpm phoenix sync
|
|
@@ -90,7 +91,7 @@ Tell us what is broken, confusing or missing, from wherever you are:
|
|
|
90
91
|
- In Studio, **Send beta feedback** in the sidebar opens a short form.
|
|
91
92
|
- In the terminal, `pnpm phoenix feedback "<message>" --type bug|idea|other` sends it with your sign-in. See the [CLI reference](?path=/docs/phoenix-phoenix-cli--docs).
|
|
92
93
|
|
|
93
|
-
Coding agents working in your project use the same command, guided by the generated `phoenix-feedback` skill. They never send on their own: an agent shows you a draft, right away when the problem is crucial and otherwise at the end of the task, and sends it only after you approve it. They are told never to include secrets, customer data or your project's source.
|
|
94
|
+
Coding agents working in your project use the same command, guided by the generated `phoenix-feedback` skill. They never send on their own: an agent shows you a draft, right away when the problem is crucial and otherwise at the end of the task, and sends it only after you approve it. They are told never to include secrets, customer data or your project's source. After a deploy goes live, the agent also asks you, once per conversation, to rate building with Phoenix from 0 to 10; it tells you the answer goes to the Phoenix team, shows the message and sends it only after you approve it.
|
|
94
95
|
|
|
95
96
|
## Known limits
|
|
96
97
|
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { type Matrix4, Vector3 } from 'three';
|
|
2
2
|
import type { DimensionUnit } from '../../dimensions/index.js';
|
|
3
|
+
/** Camera angle and an equivalent pair of bounds that contains or clamps that angle. */
|
|
4
|
+
export interface ResolvedCameraAngleBounds {
|
|
5
|
+
readonly angle: number;
|
|
6
|
+
readonly min: number;
|
|
7
|
+
readonly max: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolves periodic camera bounds around the supplied world-space angle.
|
|
11
|
+
*
|
|
12
|
+
* Equivalent angles separated by a full revolution remain the same view, so finite bounds are shifted to the
|
|
13
|
+
* revolution nearest the supplied angle before it is clamped. This prevents a preserved live angle near the 0/2π
|
|
14
|
+
* seam from introducing a full turn when the destination uses an equivalent range on another revolution.
|
|
15
|
+
*
|
|
16
|
+
* A one-sided range contains every angle on some revolution, so its finite bound is placed on the allowed side
|
|
17
|
+
* within one revolution of the angle: the angle is always inside the range and the limit is reached by orbiting
|
|
18
|
+
* toward it. Comparing a normalized angle against the raw bound instead let camera-controls snap the camera by up
|
|
19
|
+
* to a full turn on the next orbit input whenever the two happened to sit on different revolutions.
|
|
20
|
+
*/
|
|
21
|
+
export declare const resolveCameraAngleBounds: (angle: number, lowerBoundary: number, upperBoundary: number) => ResolvedCameraAngleBounds;
|
|
3
22
|
/** Camera component state that determines its orbit around a target. */
|
|
4
23
|
export interface CameraOrbitState {
|
|
5
24
|
/** Camera azimuth angle in radians. */
|
|
@@ -32,20 +51,23 @@ export interface ResolveCameraOrbitProps {
|
|
|
32
51
|
/** World transform used when the state's angles are relative. */
|
|
33
52
|
readonly worldMatrix: Matrix4;
|
|
34
53
|
}
|
|
35
|
-
/** Camera
|
|
36
|
-
export interface
|
|
37
|
-
/**
|
|
54
|
+
/** Camera rotation prepared in world angles, before applying angle bounds. */
|
|
55
|
+
export interface PreparedCameraRotation {
|
|
56
|
+
/** World azimuth angle in radians. */
|
|
38
57
|
readonly azimuthAngle: number;
|
|
39
|
-
/**
|
|
58
|
+
/** Minimum world azimuth angle in radians. */
|
|
40
59
|
readonly minAzimuthAngle: number;
|
|
41
|
-
/**
|
|
60
|
+
/** Maximum world azimuth angle in radians. */
|
|
42
61
|
readonly maxAzimuthAngle: number;
|
|
43
|
-
/**
|
|
62
|
+
/** Polar angle in radians. */
|
|
44
63
|
readonly polarAngle: number;
|
|
45
|
-
/**
|
|
64
|
+
/** Minimum polar angle in radians. */
|
|
46
65
|
readonly minPolarAngle: number;
|
|
47
|
-
/**
|
|
66
|
+
/** Maximum polar angle in radians. */
|
|
48
67
|
readonly maxPolarAngle: number;
|
|
68
|
+
}
|
|
69
|
+
/** Camera orbit prepared in world angles and default distance units, before applying angle bounds. */
|
|
70
|
+
export interface PreparedCameraOrbit extends PreparedCameraRotation {
|
|
49
71
|
/** Camera distance in domain default distance units, if configured. */
|
|
50
72
|
readonly distance: number | undefined;
|
|
51
73
|
/** Minimum camera distance in domain default distance units. */
|
|
@@ -53,8 +75,14 @@ export interface ResolvedCameraOrbit {
|
|
|
53
75
|
/** Maximum camera distance in domain default distance units. */
|
|
54
76
|
readonly maxDistance: number;
|
|
55
77
|
}
|
|
78
|
+
/** Camera orbit shared by interactive and exported rendering after applying angle bounds. */
|
|
79
|
+
export type ResolvedCameraOrbit = PreparedCameraOrbit;
|
|
80
|
+
/** Applies camera defaults, relative rotation, and distance units without clamping angles. */
|
|
81
|
+
export declare const prepareCameraOrbit: ({ state, dimensionUnit, worldMatrix, }: ResolveCameraOrbitProps) => PreparedCameraOrbit;
|
|
82
|
+
/** Applies the configured angle bounds to a prepared camera rotation. */
|
|
83
|
+
export declare const resolvePreparedCameraRotation: (cameraRotation: PreparedCameraRotation) => PreparedCameraRotation;
|
|
56
84
|
/** Resolves camera angle defaults, bounds, relative rotation, and distance units. */
|
|
57
|
-
export declare const resolveCameraOrbit: (
|
|
85
|
+
export declare const resolveCameraOrbit: (props: ResolveCameraOrbitProps) => ResolvedCameraOrbit;
|
|
58
86
|
/** Applies camera distance bounds after the caller supplies a fallback in default distance units. */
|
|
59
87
|
export declare const resolveCameraDistance: (distance: number | undefined, bounds: Pick<ResolvedCameraOrbit, 'minDistance' | 'maxDistance'>, fallbackDistance: number) => number;
|
|
60
88
|
/** Converts a camera orbit to its Cartesian offset from the look-at target. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Vector3 as
|
|
1
|
+
import{Vector3 as e}from"three";import{distanceUnitToDefault as a}from"../../dimensions/index.js";import{getWorldAzimuthAngle as n}from"./world-transform.js";let t=Math.PI/2-.3,r=2*Math.PI;export const resolveCameraAngleBounds=(e,a,n)=>{let t=Number.isFinite(a),i=Number.isFinite(n);if(!Number.isFinite(e)||!t&&!i)return{angle:e,min:Math.min(a,n),max:Math.max(a,n)};if(!t)return{angle:e,min:a,max:n+r*Math.ceil((e-n)/r)};if(!i)return{angle:e,min:a+r*Math.floor((e-a)/r),max:n};let m=n-a,l=e-((e-a)%r+r)%r,o=l+m;if(e<=o)return{angle:e,min:l,max:o};if(e-o<=l+r-e)return{angle:o,min:l,max:o};let s=l+r;return{angle:s,min:s,max:s+m}};export const prepareCameraOrbit=({state:e,dimensionUnit:r,worldMatrix:i})=>{let m=!0===e.relative?n(i):0;return{azimuthAngle:(e.azimuthAngle??-.3)+m,minAzimuthAngle:(e.minAzimuthAngle??-1/0)+m,maxAzimuthAngle:(e.maxAzimuthAngle??1/0)+m,polarAngle:e.polarAngle??t,minPolarAngle:e.minPolarAngle??0,maxPolarAngle:e.maxPolarAngle??Math.PI,distance:a(e.distance,r),minDistance:a(e.minDistance,r)??Number.EPSILON,maxDistance:a(e.maxDistance,r)??1/0}};export const resolvePreparedCameraRotation=e=>{var a;let n=resolveCameraAngleBounds(e.polarAngle,e.minPolarAngle,e.maxPolarAngle),t=resolveCameraAngleBounds(Number.isFinite(a=e.azimuthAngle)?(a%(2*Math.PI)+2*Math.PI)%(2*Math.PI):a,e.minAzimuthAngle,e.maxAzimuthAngle);return{azimuthAngle:t.angle,minAzimuthAngle:t.min,maxAzimuthAngle:t.max,polarAngle:n.angle,minPolarAngle:n.min,maxPolarAngle:n.max}};export const resolveCameraOrbit=e=>{let a=prepareCameraOrbit(e);return{...a,...resolvePreparedCameraRotation(a)}};export const resolveCameraDistance=(e,a,n)=>Math.max(a.minDistance,Math.min(e??n,a.maxDistance));export const cameraOrbitToCartesian=(a,n,t)=>new e(t*Math.sin(n)*Math.sin(a),t*Math.cos(n),t*Math.sin(n)*Math.cos(a));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { resolveBoundingBox, resolveBoundingSphere } from './bounding-shape.js';
|
|
2
|
-
export { cameraOrbitToCartesian, resolveCameraDistance, resolveCameraOrbit, type CameraOrbitState, type ResolveCameraOrbitProps, type ResolvedCameraOrbit, } from './camera-orbit.js';
|
|
2
|
+
export { cameraOrbitToCartesian, prepareCameraOrbit, resolveCameraDistance, resolveCameraOrbit, resolvePreparedCameraRotation, type CameraOrbitState, type PreparedCameraOrbit, type PreparedCameraRotation, type ResolveCameraOrbitProps, type ResolvedCameraOrbit, } from './camera-orbit.js';
|
|
3
3
|
export { toMmVec3 } from './distance-vectors.js';
|
|
4
4
|
export { createSceneObjectTransformMatrix, type SceneObjectTransformState } from './scene-object-transform.js';
|
|
5
5
|
export { getPerspectiveBoxFitDistance, getPerspectiveSphereFitDistance, getWorldToViewSpaceQuaternion, type PerspectiveBoxFitDistanceProps, type PerspectiveCameraFitFrustum, type PerspectiveCameraFitViewingAngles, type PerspectiveSphereFitDistanceProps, } from './perspective-camera-fit.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{resolveBoundingBox,resolveBoundingSphere}from"./bounding-shape.js";export{cameraOrbitToCartesian,resolveCameraDistance,resolveCameraOrbit}from"./camera-orbit.js";export{toMmVec3}from"./distance-vectors.js";export{createSceneObjectTransformMatrix}from"./scene-object-transform.js";export{getPerspectiveBoxFitDistance,getPerspectiveSphereFitDistance,getWorldToViewSpaceQuaternion}from"./perspective-camera-fit.js";export{getWorldAzimuthAngle}from"./world-transform.js";
|
|
1
|
+
export{resolveBoundingBox,resolveBoundingSphere}from"./bounding-shape.js";export{cameraOrbitToCartesian,prepareCameraOrbit,resolveCameraDistance,resolveCameraOrbit,resolvePreparedCameraRotation}from"./camera-orbit.js";export{toMmVec3}from"./distance-vectors.js";export{createSceneObjectTransformMatrix}from"./scene-object-transform.js";export{getPerspectiveBoxFitDistance,getPerspectiveSphereFitDistance,getWorldToViewSpaceQuaternion}from"./perspective-camera-fit.js";export{getWorldAzimuthAngle}from"./world-transform.js";
|
|
@@ -20,6 +20,14 @@ export type FitEntityOrthographicCameraComponentState = {
|
|
|
20
20
|
* @default true
|
|
21
21
|
*/
|
|
22
22
|
readonly animateTransition?: boolean | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Preserves the live camera zoom when switching between orthographic cameras with the same group.
|
|
25
|
+
*/
|
|
26
|
+
readonly zoomLockGroup?: string | null | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Preserves the live camera rotation when switching between cameras with the same group.
|
|
29
|
+
*/
|
|
30
|
+
readonly rotationLockGroup?: string | null | undefined;
|
|
23
31
|
/**
|
|
24
32
|
* Uses fitToBox strategy to fit the entity. By default, fitToSphere strategy is used.
|
|
25
33
|
*/
|