@fps-games/editor 0.1.1-beta.0 → 0.1.1-beta.2

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.
Files changed (51) hide show
  1. package/dist/local-editor-harness.d.ts +30 -1
  2. package/dist/local-editor-harness.d.ts.map +1 -1
  3. package/dist/local-editor-harness.js +430 -11
  4. package/dist/local-editor-harness.js.map +1 -1
  5. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts +1 -0
  6. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts.map +1 -1
  7. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js +4 -0
  8. package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js.map +1 -1
  9. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts +22 -2
  10. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts.map +1 -1
  11. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js +469 -90
  12. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js.map +1 -1
  13. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +4 -0
  14. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  15. package/node_modules/@fps-games/editor-babylon/package.json +4 -4
  16. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts +9 -1
  17. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts.map +1 -1
  18. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +77 -7
  19. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
  20. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
  21. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +4 -1
  22. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
  23. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +25 -3
  24. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  25. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +1 -1
  26. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  27. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +227 -27
  28. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  29. package/node_modules/@fps-games/editor-browser/package.json +4 -1
  30. package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts +4 -1
  31. package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts.map +1 -1
  32. package/node_modules/@fps-games/editor-core/dist/editor-session.js +13 -3
  33. package/node_modules/@fps-games/editor-core/dist/editor-session.js.map +1 -1
  34. package/node_modules/@fps-games/editor-core/dist/index.d.ts +1 -0
  35. package/node_modules/@fps-games/editor-core/dist/index.d.ts.map +1 -1
  36. package/node_modules/@fps-games/editor-core/dist/index.js +1 -0
  37. package/node_modules/@fps-games/editor-core/dist/index.js.map +1 -1
  38. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts +1 -1
  39. package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts.map +1 -1
  40. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts +78 -2
  41. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts.map +1 -1
  42. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js +81 -1
  43. package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js.map +1 -1
  44. package/node_modules/@fps-games/editor-core/dist/transform-operations.d.ts +32 -0
  45. package/node_modules/@fps-games/editor-core/dist/transform-operations.d.ts.map +1 -0
  46. package/node_modules/@fps-games/editor-core/dist/transform-operations.js +125 -0
  47. package/node_modules/@fps-games/editor-core/dist/transform-operations.js.map +1 -0
  48. package/node_modules/@fps-games/editor-core/package.json +2 -2
  49. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  50. package/node_modules/@fps-games/editor-protocol/package.json +1 -1
  51. package/package.json +6 -6
@@ -1,3 +1,22 @@
1
+ import { DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS, normalizeEditorTransformConstraint, snapEditorTransformSnapshot, } from '@fps-games/editor-core';
2
+ export function resolveBabylonTransformHandleConstraint(tool, handleKey) {
3
+ if (tool === 'move') {
4
+ if (handleKey === 'xPlaneGizmo' || handleKey === 'yPlaneGizmo' || handleKey === 'zPlaneGizmo')
5
+ return 'plane';
6
+ if (handleKey === 'xGizmo' || handleKey === 'yGizmo' || handleKey === 'zGizmo')
7
+ return 'axis';
8
+ return null;
9
+ }
10
+ if (tool === 'rotate') {
11
+ return handleKey === 'xGizmo' || handleKey === 'yGizmo' || handleKey === 'zGizmo' ? 'axis' : null;
12
+ }
13
+ if (tool === 'scale') {
14
+ if (handleKey === 'uniformScaleGizmo')
15
+ return 'uniform';
16
+ return handleKey === 'xGizmo' || handleKey === 'yGizmo' || handleKey === 'zGizmo' ? 'axis' : null;
17
+ }
18
+ return null;
19
+ }
1
20
  export function createBabylonTransformGizmoController(options) {
2
21
  const GizmoManager = options.babylon.GizmoManager;
3
22
  if (!GizmoManager)
@@ -8,13 +27,19 @@ export function createBabylonTransformGizmoController(options) {
8
27
  manager.boundingBoxGizmoEnabled = false;
9
28
  let tool = options.initialTool ?? 'select';
10
29
  let space = options.initialSpace ?? 'world';
11
- let constraint = 'axis';
30
+ let constraint = normalizeConstraintForTool(tool, 'axis') ?? 'axis';
12
31
  let selectedNodeId = null;
13
32
  let selectedNodeIds = [];
14
33
  let activeDrag = null;
15
34
  let disposed = false;
35
+ let operationSettings = cloneOperationSettings(DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS);
36
+ let pendingDuplicateDrag = false;
16
37
  let observerDisposers = [];
17
38
  let pivotProxy = null;
39
+ let freeMoveHandle = null;
40
+ let freeMoveHandleMaterial = null;
41
+ let placementMarker = null;
42
+ let placementMarkerMaterial = null;
18
43
  const canvas = options.scene.getEngine?.().getRenderingCanvas?.();
19
44
  function activeTransformTool() {
20
45
  return tool === 'select' ? null : tool;
@@ -29,6 +54,14 @@ export function createBabylonTransformGizmoController(options) {
29
54
  return [];
30
55
  return selectedNodeIds.filter(nodeId => !!options.projection.getAttachableRoot(nodeId));
31
56
  }
57
+ function getCurrentTransformTargetIds() {
58
+ return selectedNodeIds.length > 1
59
+ ? getBatchTransformTargetIds()
60
+ : [getActiveTargetId()].filter((nodeId) => !!nodeId);
61
+ }
62
+ function normalizeConstraintForTool(nextTool, nextConstraint) {
63
+ return normalizeEditorTransformConstraint(nextTool, nextConstraint);
64
+ }
32
65
  function ensurePivotProxy() {
33
66
  if (pivotProxy)
34
67
  return pivotProxy;
@@ -42,13 +75,13 @@ export function createBabylonTransformGizmoController(options) {
42
75
  };
43
76
  return pivotProxy;
44
77
  }
45
- function setPivotProxyTransform(pivot) {
78
+ function setPivotProxyTransform(pivot, rotation = { x: 0, y: 0, z: 0 }) {
46
79
  const proxy = ensurePivotProxy();
47
80
  const Vector3 = options.babylon.Vector3;
48
81
  if (!proxy || !Vector3)
49
82
  return null;
50
83
  proxy.position = new Vector3(pivot.position.x, pivot.position.y, pivot.position.z);
51
- proxy.rotation = new Vector3(0, 0, 0);
84
+ proxy.rotation = new Vector3(rotation.x, rotation.y, rotation.z);
52
85
  proxy.scaling = new Vector3(1, 1, 1);
53
86
  return proxy;
54
87
  }
@@ -68,28 +101,31 @@ export function createBabylonTransformGizmoController(options) {
68
101
  catch { }
69
102
  });
70
103
  }
71
- function registerDragObserversFor(gizmo) {
72
- const axisGizmos = [
73
- gizmo?.xGizmo,
74
- gizmo?.yGizmo,
75
- gizmo?.zGizmo,
76
- gizmo?.uniformScaleGizmo,
77
- gizmo?.xPlaneGizmo,
78
- gizmo?.yPlaneGizmo,
79
- gizmo?.zPlaneGizmo,
104
+ function registerDragObserversFor(activeTool, gizmo) {
105
+ const handleKeys = [
106
+ 'xGizmo',
107
+ 'yGizmo',
108
+ 'zGizmo',
109
+ 'uniformScaleGizmo',
110
+ 'xPlaneGizmo',
111
+ 'yPlaneGizmo',
112
+ 'zPlaneGizmo',
80
113
  ];
81
- for (const axis of axisGizmos) {
82
- const behavior = axis?.dragBehavior;
83
- addObserver(behavior?.onDragStartObservable, beginDrag);
114
+ for (const handleKey of handleKeys) {
115
+ const activeConstraint = resolveBabylonTransformHandleConstraint(activeTool, handleKey);
116
+ if (!activeConstraint)
117
+ continue;
118
+ const behavior = gizmo?.[handleKey]?.dragBehavior;
119
+ addObserver(behavior?.onDragStartObservable, () => beginDrag(activeConstraint));
84
120
  addObserver(behavior?.onDragObservable, updateDrag);
85
121
  addObserver(behavior?.onDragEndObservable, endDrag);
86
122
  }
87
123
  }
88
124
  function registerDragObservers() {
89
125
  clearDragObservers();
90
- registerDragObserversFor(manager.gizmos?.positionGizmo);
91
- registerDragObserversFor(manager.gizmos?.rotationGizmo);
92
- registerDragObserversFor(manager.gizmos?.scaleGizmo);
126
+ registerDragObserversFor('move', manager.gizmos?.positionGizmo);
127
+ registerDragObserversFor('rotate', manager.gizmos?.rotationGizmo);
128
+ registerDragObserversFor('scale', manager.gizmos?.scaleGizmo);
93
129
  }
94
130
  function applySpacePreference() {
95
131
  const matchAttachedMesh = space === 'local';
@@ -105,7 +141,15 @@ export function createBabylonTransformGizmoController(options) {
105
141
  }
106
142
  }
107
143
  catch { }
108
- for (const axis of [gizmo?.xGizmo, gizmo?.yGizmo, gizmo?.zGizmo]) {
144
+ for (const axis of [
145
+ gizmo?.xGizmo,
146
+ gizmo?.yGizmo,
147
+ gizmo?.zGizmo,
148
+ gizmo?.xPlaneGizmo,
149
+ gizmo?.yPlaneGizmo,
150
+ gizmo?.zPlaneGizmo,
151
+ gizmo?.uniformScaleGizmo,
152
+ ]) {
109
153
  try {
110
154
  if ('updateGizmoRotationToMatchAttachedMesh' in (axis ?? {})) {
111
155
  axis.updateGizmoRotationToMatchAttachedMesh = matchAttachedMesh;
@@ -115,25 +159,26 @@ export function createBabylonTransformGizmoController(options) {
115
159
  }
116
160
  }
117
161
  }
162
+ function applyHandlePreference() {
163
+ const positionGizmo = manager.gizmos?.positionGizmo;
164
+ if (positionGizmo) {
165
+ try {
166
+ positionGizmo.planarGizmoEnabled = tool === 'move';
167
+ }
168
+ catch { }
169
+ }
170
+ }
118
171
  function attachCurrentSelection() {
119
172
  const activeTool = activeTransformTool();
120
- const viewPlaneMove = activeTool === 'move' && constraint === 'view-plane';
121
- manager.positionGizmoEnabled = activeTool === 'move' && !viewPlaneMove;
173
+ manager.positionGizmoEnabled = activeTool === 'move';
122
174
  manager.rotationGizmoEnabled = activeTool === 'rotate';
123
175
  manager.scaleGizmoEnabled = activeTool === 'scale';
176
+ applyHandlePreference();
124
177
  applySpacePreference();
125
178
  registerDragObservers();
126
179
  let target = null;
127
- if (activeTool && !viewPlaneMove) {
128
- const batchTransformTargetIds = getBatchTransformTargetIds();
129
- if (batchTransformTargetIds.length > 1) {
130
- const pivot = options.projection.getSelectionPivot(batchTransformTargetIds);
131
- target = pivot ? setPivotProxyTransform(pivot) : null;
132
- }
133
- else {
134
- const activeTargetId = getActiveTargetId();
135
- target = activeTargetId ? options.projection.getAttachableRoot(activeTargetId) : null;
136
- }
180
+ if (activeTool) {
181
+ target = attachNativeTransformProxy(getCurrentTransformTargetIds());
137
182
  }
138
183
  try {
139
184
  manager.attachToNode?.(target ?? null);
@@ -141,23 +186,86 @@ export function createBabylonTransformGizmoController(options) {
141
186
  catch (error) {
142
187
  options.logger?.warn?.('[BabylonTransformGizmoController] failed to attach gizmo', error);
143
188
  }
189
+ updateFreeMoveHandle();
144
190
  }
145
- function beginDrag() {
191
+ function beginDrag(activeConstraint = constraint) {
146
192
  if (activeDrag)
147
193
  return;
148
194
  const activeTool = activeTransformTool();
149
195
  if (!activeTool)
150
196
  return;
151
- const targetIds = selectedNodeIds.length > 1
152
- ? getBatchTransformTargetIds()
153
- : [getActiveTargetId()].filter((nodeId) => !!nodeId);
154
- const drag = createActiveDrag(activeTool, targetIds, activeTool === 'move' ? constraint : 'axis');
197
+ const duplicate = pendingDuplicateDrag;
198
+ pendingDuplicateDrag = false;
199
+ const targetIds = resolveDragTargetIds(activeTool, getCurrentTransformTargetIds(), activeConstraint, duplicate);
200
+ const drag = createActiveDrag(activeTool, targetIds, activeConstraint, duplicate);
155
201
  if (!drag)
156
202
  return;
157
203
  activeDrag = drag;
158
204
  options.onDragStart?.(activeDrag);
159
205
  }
160
- function createActiveDrag(activeTool, targetIds, activeConstraint) {
206
+ function resolveDragTargetIds(activeTool, targetIds, activeConstraint, duplicate) {
207
+ if (!duplicate || targetIds.length === 0)
208
+ return targetIds;
209
+ const beforeTransforms = options.projection.readNodeTransforms(targetIds);
210
+ const validTargetIds = targetIds.filter(nodeId => !!beforeTransforms[nodeId]);
211
+ if (validTargetIds.length === 0)
212
+ return [];
213
+ const duplicateResult = options.onDuplicateDragStart?.({
214
+ targetIds: validTargetIds,
215
+ activeId: selectedNodeId && validTargetIds.includes(selectedNodeId) ? selectedNodeId : validTargetIds[validTargetIds.length - 1] ?? null,
216
+ tool: activeTool,
217
+ space,
218
+ constraint: activeConstraint,
219
+ beforeTransforms,
220
+ });
221
+ if (!duplicateResult?.targetIds.length)
222
+ return [];
223
+ selectedNodeIds = [...duplicateResult.targetIds];
224
+ selectedNodeId = duplicateResult.activeId && selectedNodeIds.includes(duplicateResult.activeId)
225
+ ? duplicateResult.activeId
226
+ : selectedNodeIds[selectedNodeIds.length - 1] ?? null;
227
+ if (!attachDragTargetIds(selectedNodeIds))
228
+ return [];
229
+ return selectedNodeIds;
230
+ }
231
+ function attachDragTargetIds(targetIds) {
232
+ const target = attachNativeTransformProxy(targetIds);
233
+ if (!target)
234
+ return false;
235
+ try {
236
+ manager.attachToNode?.(target);
237
+ updateFreeMoveHandle();
238
+ return true;
239
+ }
240
+ catch (error) {
241
+ options.logger?.warn?.('[BabylonTransformGizmoController] failed to attach duplicate drag target', error);
242
+ return false;
243
+ }
244
+ }
245
+ function attachNativeTransformProxy(targetIds) {
246
+ const validTargetIds = targetIds.filter(nodeId => !!options.projection.getAttachableRoot(nodeId));
247
+ if (validTargetIds.length === 0)
248
+ return null;
249
+ const pivot = validTargetIds.length > 1
250
+ ? options.projection.getSelectionPivot(validTargetIds)
251
+ : createSingleTargetPivot(validTargetIds[0]);
252
+ if (!pivot)
253
+ return null;
254
+ const rotation = validTargetIds.length === 1 && space === 'local'
255
+ ? options.projection.readNodeTransform(validTargetIds[0])?.rotation ?? { x: 0, y: 0, z: 0 }
256
+ : { x: 0, y: 0, z: 0 };
257
+ return setPivotProxyTransform(pivot, rotation);
258
+ }
259
+ function createSingleTargetPivot(nodeId) {
260
+ const transform = options.projection.readNodeTransform(nodeId);
261
+ return transform
262
+ ? {
263
+ mode: 'selection-center',
264
+ position: transform.position,
265
+ }
266
+ : null;
267
+ }
268
+ function createActiveDrag(activeTool, targetIds, activeConstraint, duplicate = false) {
161
269
  if (targetIds.length === 0)
162
270
  return null;
163
271
  const beforeTransforms = options.projection.readNodeTransforms(targetIds);
@@ -183,55 +291,34 @@ export function createBabylonTransformGizmoController(options) {
183
291
  pivot,
184
292
  before,
185
293
  beforeTransforms,
294
+ duplicate,
295
+ proxyStart: readNativeProxyTransform(pivot),
186
296
  };
187
297
  }
188
298
  function updateDrag() {
189
299
  if (!activeDrag)
190
300
  return;
191
- if (activeDrag.targetIds.length > 1) {
192
- const currentTransforms = previewBatchTransform(activeDrag);
193
- const current = activeDrag.activeId
194
- ? currentTransforms[activeDrag.activeId] ?? Object.values(currentTransforms)[0] ?? null
195
- : Object.values(currentTransforms)[0] ?? null;
196
- if (!current)
197
- return;
198
- options.onDragUpdate?.({ ...activeDrag, current });
199
- return;
200
- }
201
- if (!activeDrag.nodeId)
202
- return;
203
- const current = options.projection.readNodeTransform(activeDrag.nodeId);
301
+ const currentTransforms = previewBatchTransform(activeDrag);
302
+ const current = activeDrag.activeId
303
+ ? currentTransforms[activeDrag.activeId] ?? Object.values(currentTransforms)[0] ?? null
304
+ : Object.values(currentTransforms)[0] ?? null;
204
305
  if (!current)
205
306
  return;
307
+ if (activeDrag.tool === 'move') {
308
+ const pivotPosition = resolvePreviewPivotPosition(activeDrag, currentTransforms);
309
+ if (pivotPosition)
310
+ setFreeMoveHandlePosition(pivotPosition);
311
+ }
206
312
  options.onDragUpdate?.({ ...activeDrag, current });
207
313
  }
208
314
  function endDrag() {
209
315
  const drag = activeDrag;
210
316
  if (!drag)
211
317
  return;
318
+ const afterTransforms = previewBatchTransform(drag);
212
319
  activeDrag = null;
213
- if (drag.targetIds.length > 1) {
214
- const afterTransforms = previewBatchTransform(drag);
215
- emitTransformCommit(drag, afterTransforms);
216
- attachCurrentSelection();
217
- return;
218
- }
219
- if (!drag.nodeId || !drag.before) {
220
- options.onDragCancel?.(drag);
221
- return;
222
- }
223
- const after = options.projection.readNodeTransform(drag.nodeId);
224
- if (!after) {
225
- options.onDragCancel?.(drag);
226
- return;
227
- }
228
- options.onDragEnd?.({
229
- nodeId: drag.nodeId,
230
- tool: drag.tool,
231
- space: drag.space,
232
- before: drag.before,
233
- after,
234
- });
320
+ emitTransformCommit(drag, afterTransforms);
321
+ attachCurrentSelection();
235
322
  }
236
323
  function emitTransformCommit(drag, afterTransforms) {
237
324
  if (drag.targetIds.length === 1) {
@@ -246,6 +333,7 @@ export function createBabylonTransformGizmoController(options) {
246
333
  nodeId,
247
334
  tool: drag.tool,
248
335
  space: drag.space,
336
+ constraint: drag.constraint,
249
337
  before,
250
338
  after,
251
339
  });
@@ -273,14 +361,19 @@ export function createBabylonTransformGizmoController(options) {
273
361
  rotation: { x: 0, y: 0, z: 0 },
274
362
  scale: { x: 1, y: 1, z: 1 },
275
363
  });
364
+ const proxyStart = drag.proxyStart ?? {
365
+ position: drag.pivot.position,
366
+ rotation: { x: 0, y: 0, z: 0 },
367
+ scale: { x: 1, y: 1, z: 1 },
368
+ };
276
369
  if (drag.tool === 'move') {
277
- return previewMoveWithDelta(drag, subtractVec3(pivotTransform.position, drag.pivot.position));
370
+ return previewMoveWithDelta(drag, subtractVec3(pivotTransform.position, proxyStart.position));
278
371
  }
279
372
  if (drag.tool === 'rotate') {
280
- return previewRotateWithDelta(drag, pivotTransform.rotation);
373
+ return previewRotateWithDelta(drag, subtractVec3(pivotTransform.rotation, proxyStart.rotation));
281
374
  }
282
375
  if (drag.tool === 'scale') {
283
- return previewScaleWithDelta(drag, pivotTransform.scale);
376
+ return previewScaleWithDelta(drag, divideVec3(pivotTransform.scale, proxyStart.scale));
284
377
  }
285
378
  return {};
286
379
  }
@@ -290,7 +383,7 @@ export function createBabylonTransformGizmoController(options) {
290
383
  const before = drag.beforeTransforms[nodeId];
291
384
  if (!before)
292
385
  continue;
293
- transforms[nodeId] = translateTransform(before, delta);
386
+ transforms[nodeId] = applySnapToTransform(drag, nodeId, translateTransform(before, delta));
294
387
  }
295
388
  options.projection.setNodeTransformsPreview(transforms);
296
389
  return transforms;
@@ -314,6 +407,7 @@ export function createBabylonTransformGizmoController(options) {
314
407
  rotation: addVec3(before.rotation, rotationDelta),
315
408
  scale: before.scale,
316
409
  };
410
+ transforms[nodeId] = applySnapToTransform(drag, nodeId, transforms[nodeId]);
317
411
  }
318
412
  options.projection.setNodeTransformsPreview(transforms);
319
413
  return transforms;
@@ -335,10 +429,26 @@ export function createBabylonTransformGizmoController(options) {
335
429
  rotation: before.rotation,
336
430
  scale: multiplyVec3(before.scale, safeScale),
337
431
  };
432
+ transforms[nodeId] = applySnapToTransform(drag, nodeId, transforms[nodeId]);
338
433
  }
339
434
  options.projection.setNodeTransformsPreview(transforms);
340
435
  return transforms;
341
436
  }
437
+ function applySnapToTransform(drag, nodeId, after) {
438
+ if (!operationSettings.snap.enabled)
439
+ return after;
440
+ const before = drag.beforeTransforms[nodeId];
441
+ if (!before)
442
+ return after;
443
+ return snapEditorTransformSnapshot(before, after, drag.tool, operationSettings.snap);
444
+ }
445
+ function readNativeProxyTransform(pivot) {
446
+ return readProjectionLikeTransform(pivotProxy, {
447
+ position: pivot.position,
448
+ rotation: { x: 0, y: 0, z: 0 },
449
+ scale: { x: 1, y: 1, z: 1 },
450
+ });
451
+ }
342
452
  function readProjectionLikeTransform(node, fallback) {
343
453
  const rotation = node?.rotationQuaternion?.toEulerAngles?.() ?? node?.rotation;
344
454
  return {
@@ -387,6 +497,13 @@ export function createBabylonTransformGizmoController(options) {
387
497
  z: left.z * right.z,
388
498
  };
389
499
  }
500
+ function divideVec3(left, right) {
501
+ return {
502
+ x: Math.abs(right.x) > 0.000001 ? left.x / right.x : 1,
503
+ y: Math.abs(right.y) > 0.000001 ? left.y / right.y : 1,
504
+ z: Math.abs(right.z) > 0.000001 ? left.z / right.z : 1,
505
+ };
506
+ }
390
507
  function rotateVec3Euler(value, rotation) {
391
508
  return rotateZ(rotateY(rotateX(value, rotation.x), rotation.y), rotation.z);
392
509
  }
@@ -418,13 +535,15 @@ export function createBabylonTransformGizmoController(options) {
418
535
  };
419
536
  }
420
537
  function isViewPlaneMoveCandidate(event) {
421
- if (disposed || activeDrag || tool !== 'move' || constraint !== 'view-plane' || event.button !== 0)
538
+ if (disposed || activeDrag || tool !== 'move' || event.button !== 0)
422
539
  return false;
423
- const targetIds = selectedNodeIds.length > 1
424
- ? getBatchTransformTargetIds()
425
- : [getActiveTargetId()].filter((nodeId) => !!nodeId);
540
+ const targetIds = getCurrentTransformTargetIds();
426
541
  if (targetIds.length === 0)
427
542
  return false;
543
+ if (pickFreeMoveHandleAt(event.clientX, event.clientY))
544
+ return true;
545
+ if (constraint !== 'free')
546
+ return false;
428
547
  const pickedId = options.projection.pickNodeIdAt(event.clientX, event.clientY);
429
548
  return !!pickedId && targetIds.includes(pickedId);
430
549
  }
@@ -432,18 +551,22 @@ export function createBabylonTransformGizmoController(options) {
432
551
  if (disposed || activeDrag || event.button !== 0)
433
552
  return false;
434
553
  const activeTool = activeTransformTool();
435
- if (!activeTool || (activeTool === 'move' && constraint === 'view-plane'))
554
+ if (!activeTool)
436
555
  return false;
437
556
  const picked = pickGizmoMeshAt(event.clientX, event.clientY);
438
- return !!picked && isGizmoNode(picked);
557
+ if (isFreeMoveHandleNode(picked))
558
+ return false;
559
+ const candidate = !!picked && isGizmoNode(picked);
560
+ if (candidate)
561
+ pendingDuplicateDrag = isDuplicateDragModifier(event);
562
+ return candidate;
439
563
  }
440
564
  function beginViewPlaneMove(event) {
441
565
  if (!isViewPlaneMoveCandidate(event))
442
566
  return false;
443
- const targetIds = selectedNodeIds.length > 1
444
- ? getBatchTransformTargetIds()
445
- : [getActiveTargetId()].filter((nodeId) => !!nodeId);
446
- const drag = createActiveDrag('move', targetIds, 'view-plane');
567
+ const duplicate = isDuplicateDragModifier(event);
568
+ const targetIds = resolveDragTargetIds('move', getCurrentTransformTargetIds(), 'free', duplicate);
569
+ const drag = createActiveDrag('move', targetIds, 'free', duplicate);
447
570
  if (!drag)
448
571
  return false;
449
572
  const startPoint = projectPointerToViewPlane(event.clientX, event.clientY, drag.pivot.position);
@@ -457,6 +580,9 @@ export function createBabylonTransformGizmoController(options) {
457
580
  options.onDragStart?.(drag);
458
581
  return true;
459
582
  }
583
+ function isDuplicateDragModifier(event) {
584
+ return event.altKey === true;
585
+ }
460
586
  function updateViewPlaneMove(event) {
461
587
  const drag = activeDrag;
462
588
  if (!drag?.viewPlane || drag.viewPlane.pointerId !== event.pointerId || disposed)
@@ -464,7 +590,9 @@ export function createBabylonTransformGizmoController(options) {
464
590
  const currentPoint = projectPointerToViewPlane(event.clientX, event.clientY, drag.pivot.position);
465
591
  if (!currentPoint)
466
592
  return false;
467
- const transforms = previewMoveWithDelta(drag, subtractVec3(currentPoint, drag.viewPlane.startPoint));
593
+ const delta = subtractVec3(currentPoint, drag.viewPlane.startPoint);
594
+ const transforms = previewMoveWithDelta(drag, delta);
595
+ setFreeMoveHandlePosition(resolvePreviewPivotPosition(drag, transforms) ?? addVec3(drag.pivot.position, delta));
468
596
  const current = drag.activeId
469
597
  ? transforms[drag.activeId] ?? Object.values(transforms)[0] ?? null
470
598
  : Object.values(transforms)[0] ?? null;
@@ -485,6 +613,16 @@ export function createBabylonTransformGizmoController(options) {
485
613
  attachCurrentSelection();
486
614
  return true;
487
615
  }
616
+ function resolvePreviewPivotPosition(drag, transforms) {
617
+ for (const nodeId of drag.targetIds) {
618
+ const before = drag.beforeTransforms[nodeId];
619
+ const after = transforms[nodeId];
620
+ if (!before || !after)
621
+ continue;
622
+ return addVec3(drag.pivot.position, subtractVec3(after.position, before.position));
623
+ }
624
+ return null;
625
+ }
488
626
  function projectPointerToViewPlane(clientX, clientY, planePoint) {
489
627
  const camera = options.scene.activeCamera ?? options.scene.cameraToUseForPointers ?? null;
490
628
  const Vector3 = options.babylon.Vector3;
@@ -516,6 +654,113 @@ export function createBabylonTransformGizmoController(options) {
516
654
  const forwardZ = options.scene.useRightHandedSystem ? -1 : 1;
517
655
  return readVec3Like(camera.getDirection(new Vector3(0, 0, forwardZ)));
518
656
  }
657
+ function pickPlacementHit(clientX, clientY, mode) {
658
+ if (mode === 'off')
659
+ return null;
660
+ if (mode === 'ground')
661
+ return pickGroundPlacementHit(clientX, clientY);
662
+ return pickSurfacePlacementHit(clientX, clientY);
663
+ }
664
+ function pickGroundPlacementHit(clientX, clientY) {
665
+ const ray = createScenePointerRay(clientX, clientY);
666
+ const origin = readVec3Like(ray?.origin);
667
+ const direction = readVec3Like(ray?.direction);
668
+ if (!origin || !direction || Math.abs(direction.y) < 0.000001)
669
+ return null;
670
+ const t = -origin.y / direction.y;
671
+ if (!Number.isFinite(t) || t < 0)
672
+ return null;
673
+ return {
674
+ mode: 'ground',
675
+ position: addVec3(origin, scaleVec3(direction, t)),
676
+ normal: { x: 0, y: 1, z: 0 },
677
+ nodeId: null,
678
+ };
679
+ }
680
+ function pickSurfacePlacementHit(clientX, clientY) {
681
+ if (!canvas || typeof options.scene.pick !== 'function')
682
+ return null;
683
+ const rect = canvas.getBoundingClientRect();
684
+ const pick = options.scene.pick(clientX - rect.left, clientY - rect.top, (mesh) => !isPlacementPickIgnored(mesh));
685
+ const point = readVec3Like(pick?.pickedPoint);
686
+ if (!pick?.hit || !point)
687
+ return null;
688
+ const normal = readVec3Like(pick?.getNormal?.(true)) ?? readVec3Like(pick?.normal);
689
+ return {
690
+ mode: 'surface',
691
+ position: point,
692
+ normal: normal ?? undefined,
693
+ nodeId: options.projection.resolveProjectionNodeId(pick.pickedMesh ?? null),
694
+ };
695
+ }
696
+ function isPlacementPickIgnored(node) {
697
+ let current = node;
698
+ while (current) {
699
+ if (current.metadata?.editorProjectionHelper)
700
+ return true;
701
+ if (current.metadata?.editorTransformFreeMoveHandle)
702
+ return true;
703
+ if (current.metadata?.editorPlacementMarker)
704
+ return true;
705
+ current = current.parent ?? null;
706
+ }
707
+ return false;
708
+ }
709
+ function createScenePointerRay(clientX, clientY) {
710
+ const camera = options.scene.activeCamera ?? options.scene.cameraToUseForPointers ?? null;
711
+ const Matrix = options.babylon.Matrix;
712
+ if (!canvas || !camera || !Matrix?.Identity || !options.scene.createPickingRay)
713
+ return null;
714
+ const rect = canvas.getBoundingClientRect();
715
+ return options.scene.createPickingRay(clientX - rect.left, clientY - rect.top, Matrix.Identity(), camera);
716
+ }
717
+ function ensurePlacementMarker() {
718
+ if (placementMarker)
719
+ return placementMarker;
720
+ const MeshBuilder = options.babylon.MeshBuilder;
721
+ const StandardMaterial = options.babylon.StandardMaterial;
722
+ const Color3 = options.babylon.Color3;
723
+ const utilityScene = manager.utilityLayer?.utilityLayerScene;
724
+ if (!MeshBuilder?.CreateSphere || !utilityScene)
725
+ return null;
726
+ placementMarker = MeshBuilder.CreateSphere('editor.placement.marker', { diameter: 0.32, segments: 16 }, utilityScene);
727
+ placementMarker.metadata = {
728
+ ...(placementMarker.metadata ?? {}),
729
+ editorProjectionHelper: true,
730
+ editorPlacementMarker: true,
731
+ };
732
+ placementMarker.isPickable = false;
733
+ if (StandardMaterial && Color3) {
734
+ placementMarkerMaterial = new StandardMaterial('editor.placement.marker.material', utilityScene);
735
+ placementMarkerMaterial.diffuseColor = new Color3(0.18, 0.86, 0.78);
736
+ placementMarkerMaterial.emissiveColor = new Color3(0.1, 0.62, 0.56);
737
+ placementMarkerMaterial.specularColor = new Color3(0.04, 0.18, 0.16);
738
+ placementMarker.material = placementMarkerMaterial;
739
+ }
740
+ setPlacementMarkerVisible(false);
741
+ return placementMarker;
742
+ }
743
+ function setPlacementMarkerVisible(visible) {
744
+ if (!placementMarker)
745
+ return;
746
+ placementMarker.isVisible = visible;
747
+ try {
748
+ placementMarker.setEnabled?.(visible);
749
+ }
750
+ catch { }
751
+ }
752
+ function setPlacementMarker(hit) {
753
+ if (!hit) {
754
+ setPlacementMarkerVisible(false);
755
+ return;
756
+ }
757
+ const marker = ensurePlacementMarker();
758
+ const Vector3 = options.babylon.Vector3;
759
+ if (!marker || !Vector3)
760
+ return;
761
+ marker.position = new Vector3(hit.position.x, hit.position.y, hit.position.z);
762
+ setPlacementMarkerVisible(true);
763
+ }
519
764
  function pickGizmoMeshAt(clientX, clientY) {
520
765
  if (!canvas)
521
766
  return null;
@@ -529,10 +774,88 @@ export function createBabylonTransformGizmoController(options) {
529
774
  const scenePick = typeof options.scene.pick === 'function' ? options.scene.pick(x, y) : null;
530
775
  return scenePick?.hit ? scenePick.pickedMesh ?? null : null;
531
776
  }
777
+ function pickFreeMoveHandleAt(clientX, clientY) {
778
+ if (!canvas || !freeMoveHandle)
779
+ return null;
780
+ const rect = canvas.getBoundingClientRect();
781
+ const x = clientX - rect.left;
782
+ const y = clientY - rect.top;
783
+ const utilityScene = manager.utilityLayer?.utilityLayerScene;
784
+ const pick = typeof utilityScene?.pick === 'function'
785
+ ? utilityScene.pick(x, y, (mesh) => isFreeMoveHandleNode(mesh))
786
+ : null;
787
+ return pick?.hit && isFreeMoveHandleNode(pick.pickedMesh) ? pick.pickedMesh : null;
788
+ }
789
+ function ensureFreeMoveHandle() {
790
+ if (freeMoveHandle)
791
+ return freeMoveHandle;
792
+ const MeshBuilder = options.babylon.MeshBuilder;
793
+ const StandardMaterial = options.babylon.StandardMaterial;
794
+ const Color3 = options.babylon.Color3;
795
+ const utilityScene = manager.utilityLayer?.utilityLayerScene;
796
+ if (!MeshBuilder?.CreateSphere || !utilityScene)
797
+ return null;
798
+ freeMoveHandle = MeshBuilder.CreateSphere('editor.transform.freeMoveHandle', { diameter: 0.22, segments: 12 }, utilityScene);
799
+ freeMoveHandle.metadata = {
800
+ ...(freeMoveHandle.metadata ?? {}),
801
+ editorProjectionHelper: true,
802
+ editorTransformFreeMoveHandle: true,
803
+ };
804
+ freeMoveHandle.isPickable = true;
805
+ if (StandardMaterial && Color3) {
806
+ freeMoveHandleMaterial = new StandardMaterial('editor.transform.freeMoveHandle.material', utilityScene);
807
+ freeMoveHandleMaterial.diffuseColor = new Color3(1, 0.82, 0.28);
808
+ freeMoveHandleMaterial.emissiveColor = new Color3(1, 0.68, 0.18);
809
+ freeMoveHandleMaterial.specularColor = new Color3(0.2, 0.18, 0.08);
810
+ freeMoveHandle.material = freeMoveHandleMaterial;
811
+ }
812
+ setFreeMoveHandleVisible(false);
813
+ return freeMoveHandle;
814
+ }
815
+ function setFreeMoveHandleVisible(visible) {
816
+ if (!freeMoveHandle)
817
+ return;
818
+ freeMoveHandle.isVisible = visible;
819
+ try {
820
+ freeMoveHandle.setEnabled?.(visible);
821
+ }
822
+ catch { }
823
+ }
824
+ function setFreeMoveHandlePosition(position) {
825
+ const handle = ensureFreeMoveHandle();
826
+ const Vector3 = options.babylon.Vector3;
827
+ if (!handle || !Vector3)
828
+ return;
829
+ handle.position = new Vector3(position.x, position.y, position.z);
830
+ setFreeMoveHandleVisible(true);
831
+ }
832
+ function updateFreeMoveHandle() {
833
+ if (tool !== 'move' || activeDrag) {
834
+ setFreeMoveHandleVisible(false);
835
+ return;
836
+ }
837
+ const targetIds = getCurrentTransformTargetIds();
838
+ if (targetIds.length === 0) {
839
+ setFreeMoveHandleVisible(false);
840
+ return;
841
+ }
842
+ const pivot = targetIds.length > 1
843
+ ? options.projection.getSelectionPivot(targetIds)
844
+ : null;
845
+ const position = pivot?.position
846
+ ?? (targetIds[0] ? options.projection.readNodeTransform(targetIds[0])?.position : null);
847
+ if (!position) {
848
+ setFreeMoveHandleVisible(false);
849
+ return;
850
+ }
851
+ setFreeMoveHandlePosition(position);
852
+ }
532
853
  function isGizmoNode(node) {
533
854
  let current = node;
534
855
  const roots = collectCurrentGizmoRoots();
535
856
  while (current) {
857
+ if (isFreeMoveHandleNode(current))
858
+ return false;
536
859
  if (roots.has(current))
537
860
  return true;
538
861
  if (current.metadata?.editorProjection?.nodeId)
@@ -546,6 +869,15 @@ export function createBabylonTransformGizmoController(options) {
546
869
  }
547
870
  return false;
548
871
  }
872
+ function isFreeMoveHandleNode(node) {
873
+ let current = node;
874
+ while (current) {
875
+ if (current.metadata?.editorTransformFreeMoveHandle)
876
+ return true;
877
+ current = current.parent ?? null;
878
+ }
879
+ return false;
880
+ }
549
881
  function collectCurrentGizmoRoots() {
550
882
  const roots = new Set();
551
883
  const activeTool = activeTransformTool();
@@ -591,6 +923,7 @@ export function createBabylonTransformGizmoController(options) {
591
923
  return;
592
924
  controller.cancelDrag();
593
925
  tool = nextTool;
926
+ constraint = normalizeConstraintForTool(tool, constraint) ?? 'axis';
594
927
  attachCurrentSelection();
595
928
  },
596
929
  setSpace(nextSpace) {
@@ -605,12 +938,25 @@ export function createBabylonTransformGizmoController(options) {
605
938
  setConstraint(nextConstraint) {
606
939
  if (disposed)
607
940
  return;
608
- if (constraint === nextConstraint)
941
+ const normalized = normalizeConstraintForTool(tool, nextConstraint) ?? 'axis';
942
+ if (constraint === normalized)
609
943
  return;
610
944
  controller.cancelDrag();
611
- constraint = nextConstraint;
945
+ constraint = normalized;
612
946
  attachCurrentSelection();
613
947
  },
948
+ setOperationSettings(settings) {
949
+ if (disposed)
950
+ return;
951
+ operationSettings = cloneOperationSettings(settings);
952
+ if (activeDrag)
953
+ updateDrag();
954
+ },
955
+ preparePointerDrag(event) {
956
+ if (disposed || event.button !== 0)
957
+ return;
958
+ pendingDuplicateDrag = isDuplicateDragModifier(event);
959
+ },
614
960
  setSelectedNode(nextNodeId) {
615
961
  if (disposed)
616
962
  return;
@@ -648,6 +994,8 @@ export function createBabylonTransformGizmoController(options) {
648
994
  beginViewPlaneMove,
649
995
  updateViewPlaneMove,
650
996
  endViewPlaneMove,
997
+ pickPlacementHit,
998
+ setPlacementMarker,
651
999
  cancelDrag() {
652
1000
  const drag = activeDrag;
653
1001
  if (!drag)
@@ -655,11 +1003,11 @@ export function createBabylonTransformGizmoController(options) {
655
1003
  activeDrag = null;
656
1004
  if (drag.targetIds.length > 1) {
657
1005
  options.projection.setNodeTransformsPreview(drag.beforeTransforms);
658
- attachCurrentSelection();
659
1006
  }
660
1007
  else if (drag.nodeId && drag.before) {
661
1008
  options.projection.setNodeTransformPreview(drag.nodeId, drag.before);
662
1009
  }
1010
+ attachCurrentSelection();
663
1011
  options.onDragCancel?.(drag);
664
1012
  },
665
1013
  dispose() {
@@ -679,11 +1027,42 @@ export function createBabylonTransformGizmoController(options) {
679
1027
  pivotProxy?.dispose?.();
680
1028
  }
681
1029
  catch { }
1030
+ try {
1031
+ freeMoveHandle?.dispose?.();
1032
+ }
1033
+ catch { }
1034
+ try {
1035
+ freeMoveHandleMaterial?.dispose?.();
1036
+ }
1037
+ catch { }
1038
+ try {
1039
+ placementMarker?.dispose?.();
1040
+ }
1041
+ catch { }
1042
+ try {
1043
+ placementMarkerMaterial?.dispose?.();
1044
+ }
1045
+ catch { }
682
1046
  pivotProxy = null;
1047
+ freeMoveHandle = null;
1048
+ freeMoveHandleMaterial = null;
1049
+ placementMarker = null;
1050
+ placementMarkerMaterial = null;
683
1051
  disposed = true;
684
1052
  },
685
1053
  };
686
1054
  attachCurrentSelection();
687
1055
  return controller;
688
1056
  }
1057
+ function cloneOperationSettings(settings) {
1058
+ return {
1059
+ snap: {
1060
+ enabled: settings.snap.enabled,
1061
+ moveStep: settings.snap.moveStep,
1062
+ rotateStepDegrees: settings.snap.rotateStepDegrees,
1063
+ scaleStep: settings.snap.scaleStep,
1064
+ },
1065
+ placementMode: settings.placementMode,
1066
+ };
1067
+ }
689
1068
  //# sourceMappingURL=transform-gizmo-controller.js.map