@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.
- package/dist/local-editor-harness.d.ts +30 -1
- package/dist/local-editor-harness.d.ts.map +1 -1
- package/dist/local-editor-harness.js +430 -11
- package/dist/local-editor-harness.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts +1 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js +4 -0
- package/node_modules/@fps-games/editor-babylon/dist/scene-view-input-controller.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts +22 -2
- package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js +469 -90
- package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js.map +1 -1
- package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +4 -0
- package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-babylon/package.json +4 -4
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts +9 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js +77 -7
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shared.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js +4 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-shortcuts.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +25 -3
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +227 -27
- package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
- package/node_modules/@fps-games/editor-browser/package.json +4 -1
- package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts +4 -1
- package/node_modules/@fps-games/editor-core/dist/editor-session.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/editor-session.js +13 -3
- package/node_modules/@fps-games/editor-core/dist/editor-session.js.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/index.d.ts +1 -0
- package/node_modules/@fps-games/editor-core/dist/index.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/index.js +1 -0
- package/node_modules/@fps-games/editor-core/dist/index.js.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts +1 -1
- package/node_modules/@fps-games/editor-core/dist/scene-view-input.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts +78 -2
- package/node_modules/@fps-games/editor-core/dist/transform-gizmo.d.ts.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js +81 -1
- package/node_modules/@fps-games/editor-core/dist/transform-gizmo.js.map +1 -1
- package/node_modules/@fps-games/editor-core/dist/transform-operations.d.ts +32 -0
- package/node_modules/@fps-games/editor-core/dist/transform-operations.d.ts.map +1 -0
- package/node_modules/@fps-games/editor-core/dist/transform-operations.js +125 -0
- package/node_modules/@fps-games/editor-core/dist/transform-operations.js.map +1 -0
- package/node_modules/@fps-games/editor-core/package.json +2 -2
- package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
- package/node_modules/@fps-games/editor-protocol/package.json +1 -1
- package/package.json +6 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEditorSession, createInspectorRegistry, createInspectorEditPayload, mergeInspectorSections, serializedMultiObjectToInspectorObject, serializedObjectToInspectorObject, validateSceneGraphDelete, validateSceneGraphDrop, validateSceneGraphGroupSelection, validateSceneGraphMove, validateSceneGraphRename, } from '@fps-games/editor-core';
|
|
1
|
+
import { createEditorSession, createInspectorRegistry, DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS, createInspectorEditPayload, computeEditorTransformActionTargets, mergeInspectorSections, normalizeEditorTransformConstraint, serializedMultiObjectToInspectorObject, serializedObjectToInspectorObject, validateSceneGraphDelete, validateSceneGraphDrop, validateSceneGraphGroupSelection, validateSceneGraphMove, validateSceneGraphRename, } from '@fps-games/editor-core';
|
|
2
2
|
import { createLocalEditorBrowserUi, } from '@fps-games/editor-browser';
|
|
3
3
|
import { createBabylonEditorProjection, createBabylonEditorWorld, createBabylonProjectionSelectionController, createBabylonSceneViewCameraController, createBabylonSceneViewInputController, createBabylonTransformGizmoController, focusEditorViewportSelection, } from '@fps-games/editor-babylon';
|
|
4
4
|
export function createLocalEditorHarness(options) {
|
|
@@ -22,6 +22,9 @@ export function createLocalEditorHarness(options) {
|
|
|
22
22
|
transformTool: 'select',
|
|
23
23
|
transformSpace: 'world',
|
|
24
24
|
transformConstraint: 'axis',
|
|
25
|
+
transformOperationSettings: cloneTransformOperationSettings(DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS),
|
|
26
|
+
duplicateDrag: null,
|
|
27
|
+
armedPlacement: null,
|
|
25
28
|
resizeHandler: null,
|
|
26
29
|
status: 'Game running',
|
|
27
30
|
statusTone: 'default',
|
|
@@ -55,7 +58,7 @@ export function createLocalEditorHarness(options) {
|
|
|
55
58
|
harness.render();
|
|
56
59
|
},
|
|
57
60
|
onCreateFromAsset: (assetId) => {
|
|
58
|
-
if (
|
|
61
|
+
if (createAssetFromBrowserIntent(state, options, assetId))
|
|
59
62
|
harness.render();
|
|
60
63
|
},
|
|
61
64
|
onSelectHierarchyItem: (input) => {
|
|
@@ -102,7 +105,9 @@ export function createLocalEditorHarness(options) {
|
|
|
102
105
|
},
|
|
103
106
|
onTransformToolChange: (tool) => {
|
|
104
107
|
state.transformTool = tool;
|
|
108
|
+
state.transformConstraint = normalizeTransformConstraint(tool, state.transformConstraint);
|
|
105
109
|
state.gizmo?.setTool(tool);
|
|
110
|
+
state.gizmo?.setConstraint(state.transformConstraint);
|
|
106
111
|
harness.render();
|
|
107
112
|
},
|
|
108
113
|
onTransformSpaceChange: (space) => {
|
|
@@ -111,10 +116,51 @@ export function createLocalEditorHarness(options) {
|
|
|
111
116
|
harness.render();
|
|
112
117
|
},
|
|
113
118
|
onTransformConstraintChange: (constraint) => {
|
|
114
|
-
state.transformConstraint = constraint;
|
|
115
|
-
state.gizmo?.setConstraint(
|
|
119
|
+
state.transformConstraint = normalizeTransformConstraint(state.transformTool, constraint);
|
|
120
|
+
state.gizmo?.setConstraint(state.transformConstraint);
|
|
116
121
|
harness.render();
|
|
117
122
|
},
|
|
123
|
+
onTransformSnapEnabledChange: (enabled) => {
|
|
124
|
+
state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, {
|
|
125
|
+
snap: {
|
|
126
|
+
...state.transformOperationSettings.snap,
|
|
127
|
+
enabled,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
state.gizmo?.setOperationSettings(state.transformOperationSettings);
|
|
131
|
+
state.status = enabled ? 'Transform snap enabled' : 'Transform snap disabled';
|
|
132
|
+
harness.render();
|
|
133
|
+
},
|
|
134
|
+
onTransformSnapStepChange: (input) => {
|
|
135
|
+
const value = normalizePositiveStep(input.value);
|
|
136
|
+
if (value == null)
|
|
137
|
+
return;
|
|
138
|
+
const snap = { ...state.transformOperationSettings.snap };
|
|
139
|
+
if (input.kind === 'move')
|
|
140
|
+
snap.moveStep = value;
|
|
141
|
+
else if (input.kind === 'rotate')
|
|
142
|
+
snap.rotateStepDegrees = value;
|
|
143
|
+
else
|
|
144
|
+
snap.scaleStep = value;
|
|
145
|
+
state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, { snap });
|
|
146
|
+
state.gizmo?.setOperationSettings(state.transformOperationSettings);
|
|
147
|
+
state.status = `Transform snap ${input.kind} step ${value}`;
|
|
148
|
+
harness.render();
|
|
149
|
+
},
|
|
150
|
+
onPlacementModeChange: (mode) => {
|
|
151
|
+
state.transformOperationSettings = updateTransformOperationSettings(state.transformOperationSettings, {
|
|
152
|
+
placementMode: normalizePlacementMode(mode),
|
|
153
|
+
});
|
|
154
|
+
state.gizmo?.setOperationSettings(state.transformOperationSettings);
|
|
155
|
+
if (state.transformOperationSettings.placementMode === 'off')
|
|
156
|
+
clearArmedPlacement(state);
|
|
157
|
+
state.status = `Placement mode: ${state.transformOperationSettings.placementMode}`;
|
|
158
|
+
harness.render();
|
|
159
|
+
},
|
|
160
|
+
onTransformAction: (action) => {
|
|
161
|
+
if (executeTransformAction(state, options, action))
|
|
162
|
+
harness.render();
|
|
163
|
+
},
|
|
118
164
|
onFocusSelection: () => {
|
|
119
165
|
if (focusSelectedProjection(state))
|
|
120
166
|
harness.render();
|
|
@@ -268,6 +314,49 @@ export function mergeLocalEditorHarnessInspectorComponentSections(input) {
|
|
|
268
314
|
}),
|
|
269
315
|
};
|
|
270
316
|
}
|
|
317
|
+
function normalizeTransformConstraint(tool, constraint) {
|
|
318
|
+
return normalizeEditorTransformConstraint(tool, constraint) ?? 'axis';
|
|
319
|
+
}
|
|
320
|
+
function cloneTransformOperationSettings(settings) {
|
|
321
|
+
return {
|
|
322
|
+
snap: {
|
|
323
|
+
enabled: settings.snap.enabled,
|
|
324
|
+
moveStep: settings.snap.moveStep,
|
|
325
|
+
rotateStepDegrees: settings.snap.rotateStepDegrees,
|
|
326
|
+
scaleStep: settings.snap.scaleStep,
|
|
327
|
+
},
|
|
328
|
+
placementMode: settings.placementMode,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function updateTransformOperationSettings(current, patch) {
|
|
332
|
+
return {
|
|
333
|
+
...current,
|
|
334
|
+
...patch,
|
|
335
|
+
snap: patch.snap
|
|
336
|
+
? { ...current.snap, ...patch.snap }
|
|
337
|
+
: { ...current.snap },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function normalizePositiveStep(value) {
|
|
341
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
342
|
+
return null;
|
|
343
|
+
return Number(value.toFixed(4));
|
|
344
|
+
}
|
|
345
|
+
function normalizePlacementMode(mode) {
|
|
346
|
+
return mode === 'ground' || mode === 'surface' ? mode : 'off';
|
|
347
|
+
}
|
|
348
|
+
function validateTransformActionSelection(state, action) {
|
|
349
|
+
const selection = state.session?.getState().selection;
|
|
350
|
+
const selectedCount = selection?.selectedIds.length ?? 0;
|
|
351
|
+
if (action.startsWith('align-')) {
|
|
352
|
+
return selectedCount >= 2 && selection?.activeId
|
|
353
|
+
? { ok: true }
|
|
354
|
+
: { ok: false, message: 'Align needs at least 2 selected objects and an active object' };
|
|
355
|
+
}
|
|
356
|
+
return selectedCount >= 3
|
|
357
|
+
? { ok: true }
|
|
358
|
+
: { ok: false, message: 'Distribute needs at least 3 selected objects' };
|
|
359
|
+
}
|
|
271
360
|
async function createEditorWorld(state, options, render) {
|
|
272
361
|
disposeEditorWorld(state);
|
|
273
362
|
const canvas = options.worldAdapter.getCanvas();
|
|
@@ -301,8 +390,8 @@ async function createEditorWorld(state, options, render) {
|
|
|
301
390
|
logger: console,
|
|
302
391
|
onDragStart(event) {
|
|
303
392
|
state.status = event.targetIds.length > 1
|
|
304
|
-
? `Dragging ${event.tool} ${event.targetIds.length} objects`
|
|
305
|
-
: `Dragging ${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
|
|
393
|
+
? `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.targetIds.length} objects`
|
|
394
|
+
: `Dragging ${event.duplicate ? 'duplicate ' : ''}${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
|
|
306
395
|
render();
|
|
307
396
|
},
|
|
308
397
|
onDragUpdate() {
|
|
@@ -313,12 +402,20 @@ async function createEditorWorld(state, options, render) {
|
|
|
313
402
|
render();
|
|
314
403
|
},
|
|
315
404
|
onDragCancel(event) {
|
|
405
|
+
if (event.duplicate && cancelDuplicateDrag(state, options)) {
|
|
406
|
+
render();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
316
409
|
state.status = event.targetIds.length > 1
|
|
317
410
|
? `Canceled ${event.tool} ${event.targetIds.length} objects`
|
|
318
411
|
: `Canceled ${event.tool} ${event.nodeId ?? event.activeId ?? 'selection'}`;
|
|
319
412
|
render();
|
|
320
413
|
},
|
|
414
|
+
onDuplicateDragStart(input) {
|
|
415
|
+
return beginDuplicateDrag(state, options, input);
|
|
416
|
+
},
|
|
321
417
|
});
|
|
418
|
+
gizmo.setOperationSettings(state.transformOperationSettings);
|
|
322
419
|
gizmo.setConstraint(state.transformConstraint);
|
|
323
420
|
const selectionController = createBabylonProjectionSelectionController({
|
|
324
421
|
scene: world.scene,
|
|
@@ -348,7 +445,17 @@ async function createEditorWorld(state, options, render) {
|
|
|
348
445
|
isGizmoDragCandidate: (event) => gizmo.isGizmoDragCandidate(event),
|
|
349
446
|
isBoxSelectCandidate: (event) => selectionController.isBoxSelectionCandidate(event),
|
|
350
447
|
isViewPlaneMoveCandidate: (event) => gizmo.isViewPlaneMoveCandidate(event),
|
|
448
|
+
isPlacementCandidate: () => isPlacementArmed(state),
|
|
351
449
|
onPointerIntentStart(event) {
|
|
450
|
+
if (event.state.intent === 'gizmo-drag') {
|
|
451
|
+
gizmo.preparePointerDrag(event.originalEvent);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (event.state.intent === 'placement') {
|
|
455
|
+
if (previewArmedPlacement(state, event.originalEvent))
|
|
456
|
+
render();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
352
459
|
if (event.state.intent === 'view-plane-move') {
|
|
353
460
|
if (gizmo.beginViewPlaneMove(event.originalEvent))
|
|
354
461
|
render();
|
|
@@ -359,6 +466,11 @@ async function createEditorWorld(state, options, render) {
|
|
|
359
466
|
}
|
|
360
467
|
},
|
|
361
468
|
onPointerIntentMove(event) {
|
|
469
|
+
if (event.state.intent === 'placement') {
|
|
470
|
+
if (previewArmedPlacement(state, event.originalEvent))
|
|
471
|
+
render();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
362
474
|
if (event.state.intent === 'view-plane-move') {
|
|
363
475
|
if (gizmo.updateViewPlaneMove(event.originalEvent))
|
|
364
476
|
render();
|
|
@@ -373,6 +485,11 @@ async function createEditorWorld(state, options, render) {
|
|
|
373
485
|
}
|
|
374
486
|
},
|
|
375
487
|
onPointerIntentEnd(event) {
|
|
488
|
+
if (event.state.intent === 'placement') {
|
|
489
|
+
if (commitArmedPlacement(state, options, event.originalEvent))
|
|
490
|
+
render();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
376
493
|
if (event.state.intent === 'view-plane-move') {
|
|
377
494
|
if (gizmo.endViewPlaneMove(event.originalEvent))
|
|
378
495
|
render();
|
|
@@ -383,6 +500,11 @@ async function createEditorWorld(state, options, render) {
|
|
|
383
500
|
}
|
|
384
501
|
},
|
|
385
502
|
onPointerIntentCancel(event) {
|
|
503
|
+
if (event.state.intent === 'placement') {
|
|
504
|
+
state.gizmo?.setPlacementMarker(null);
|
|
505
|
+
render();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
386
508
|
if (event.state.intent === 'view-plane-move') {
|
|
387
509
|
gizmo.cancelDrag();
|
|
388
510
|
render();
|
|
@@ -803,6 +925,193 @@ function syncSelectionToProjection(state, selection) {
|
|
|
803
925
|
state.gizmo?.setSelection(selection);
|
|
804
926
|
state.gizmo?.refreshSelection();
|
|
805
927
|
}
|
|
928
|
+
function executeTransformAction(state, options, action) {
|
|
929
|
+
if (state.mode !== 'editor' || !state.session)
|
|
930
|
+
return false;
|
|
931
|
+
const document = state.session.getState().workingDocument;
|
|
932
|
+
const selection = state.session.getState().selection;
|
|
933
|
+
cancelActiveOperation(state);
|
|
934
|
+
const validation = validateTransformActionSelection(state, action);
|
|
935
|
+
if (!validation.ok) {
|
|
936
|
+
state.status = validation.message;
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
const beforeTransforms = state.projection?.readNodeTransforms(selection.selectedIds) ?? {};
|
|
940
|
+
const transformTargets = selection.selectedIds
|
|
941
|
+
.map((id) => {
|
|
942
|
+
const transform = beforeTransforms[id];
|
|
943
|
+
return transform ? { id, transform } : null;
|
|
944
|
+
})
|
|
945
|
+
.filter((target) => !!target);
|
|
946
|
+
const targets = computeEditorTransformActionTargets({
|
|
947
|
+
action,
|
|
948
|
+
activeId: selection.activeId,
|
|
949
|
+
targets: transformTargets,
|
|
950
|
+
});
|
|
951
|
+
if (targets.length === 0) {
|
|
952
|
+
state.status = `Transform action rejected: ${action}`;
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
const changedTargets = targets.filter(target => !editorTransformSnapshotsEqual(target.before, target.after));
|
|
956
|
+
if (changedTargets.length === 0) {
|
|
957
|
+
state.status = `Transform action unchanged: ${action}`;
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
const patch = options.documentAdapter.createTransformBatchPatch?.({
|
|
961
|
+
document,
|
|
962
|
+
targetIds: changedTargets.map(target => target.id),
|
|
963
|
+
activeId: selection.activeId,
|
|
964
|
+
tool: 'move',
|
|
965
|
+
space: 'world',
|
|
966
|
+
constraint: action === 'align-all' ? 'free' : 'axis',
|
|
967
|
+
pivot: createTransformActionPivot(targets),
|
|
968
|
+
targets: changedTargets,
|
|
969
|
+
});
|
|
970
|
+
if (!patch) {
|
|
971
|
+
state.status = `Transform action ignored: ${action}`;
|
|
972
|
+
return true;
|
|
973
|
+
}
|
|
974
|
+
const result = state.session.dispatch({
|
|
975
|
+
type: 'document.patch',
|
|
976
|
+
label: patch.label ?? formatTransformActionStatus(action, changedTargets.length),
|
|
977
|
+
patch: patch.patch,
|
|
978
|
+
targetId: selection.activeId ?? changedTargets[0]?.id,
|
|
979
|
+
});
|
|
980
|
+
if (!result.documentChanged) {
|
|
981
|
+
state.status = `Transform action unchanged: ${action}`;
|
|
982
|
+
return true;
|
|
983
|
+
}
|
|
984
|
+
const changedIds = patch.changedIds ?? changedTargets.map(target => target.id);
|
|
985
|
+
syncProjectionForChangedIds(state, options, result.workingDocument, changedIds);
|
|
986
|
+
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
987
|
+
state.status = patch.label ?? formatTransformActionStatus(action, changedTargets.length);
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
function createTransformActionPivot(targets) {
|
|
991
|
+
const center = averageTransformTargetPositions(targets);
|
|
992
|
+
return {
|
|
993
|
+
mode: 'selection-center',
|
|
994
|
+
position: center,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function averageTransformTargetPositions(targets) {
|
|
998
|
+
if (targets.length === 0)
|
|
999
|
+
return { x: 0, y: 0, z: 0 };
|
|
1000
|
+
return {
|
|
1001
|
+
x: targets.reduce((sum, target) => sum + target.before.position.x, 0) / targets.length,
|
|
1002
|
+
y: targets.reduce((sum, target) => sum + target.before.position.y, 0) / targets.length,
|
|
1003
|
+
z: targets.reduce((sum, target) => sum + target.before.position.z, 0) / targets.length,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function editorTransformSnapshotsEqual(left, right) {
|
|
1007
|
+
return editorVec3Equal(left.position, right.position)
|
|
1008
|
+
&& editorVec3Equal(left.rotation, right.rotation)
|
|
1009
|
+
&& editorVec3Equal(left.scale, right.scale);
|
|
1010
|
+
}
|
|
1011
|
+
function editorVec3Equal(left, right) {
|
|
1012
|
+
return Math.abs(left.x - right.x) < 0.000000001
|
|
1013
|
+
&& Math.abs(left.y - right.y) < 0.000000001
|
|
1014
|
+
&& Math.abs(left.z - right.z) < 0.000000001;
|
|
1015
|
+
}
|
|
1016
|
+
function formatTransformActionStatus(action, count) {
|
|
1017
|
+
return `${action} ${count} object${count === 1 ? '' : 's'}`;
|
|
1018
|
+
}
|
|
1019
|
+
function createAssetFromBrowserIntent(state, options, assetId) {
|
|
1020
|
+
if (state.transformOperationSettings.placementMode === 'off') {
|
|
1021
|
+
return addAssetToDocument(state, options, assetId);
|
|
1022
|
+
}
|
|
1023
|
+
return armAssetPlacement(state, options, assetId);
|
|
1024
|
+
}
|
|
1025
|
+
function armAssetPlacement(state, options, assetId) {
|
|
1026
|
+
if (state.mode !== 'editor')
|
|
1027
|
+
return false;
|
|
1028
|
+
cancelActiveOperation(state);
|
|
1029
|
+
const asset = state.assets.find(candidate => resolveAssetId(options, candidate) === assetId);
|
|
1030
|
+
if (!asset)
|
|
1031
|
+
return false;
|
|
1032
|
+
state.armedPlacement = { assetId, asset };
|
|
1033
|
+
state.gizmo?.setPlacementMarker(null);
|
|
1034
|
+
state.status = `Placement armed: ${formatAssetLabel(asset, assetId)} (${state.transformOperationSettings.placementMode})`;
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
function isPlacementArmed(state) {
|
|
1038
|
+
return state.mode === 'editor'
|
|
1039
|
+
&& !!state.armedPlacement
|
|
1040
|
+
&& state.transformOperationSettings.placementMode !== 'off';
|
|
1041
|
+
}
|
|
1042
|
+
function previewArmedPlacement(state, event) {
|
|
1043
|
+
if (!isPlacementArmed(state))
|
|
1044
|
+
return false;
|
|
1045
|
+
const hit = pickArmedPlacementHit(state, event);
|
|
1046
|
+
state.gizmo?.setPlacementMarker(hit);
|
|
1047
|
+
const mode = state.transformOperationSettings.placementMode;
|
|
1048
|
+
state.status = hit
|
|
1049
|
+
? `Placement ${mode}: ${formatVec3(hit.position)}`
|
|
1050
|
+
: `Placement ${mode}: no hit`;
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
function commitArmedPlacement(state, options, event) {
|
|
1054
|
+
if (!isPlacementArmed(state) || !state.session)
|
|
1055
|
+
return false;
|
|
1056
|
+
const armed = state.armedPlacement;
|
|
1057
|
+
const session = state.session;
|
|
1058
|
+
const beforeDocument = session.getState().workingDocument;
|
|
1059
|
+
const hit = pickArmedPlacementHit(state, event);
|
|
1060
|
+
if (!armed || !hit) {
|
|
1061
|
+
state.gizmo?.setPlacementMarker(null);
|
|
1062
|
+
state.status = 'Placement rejected: no hit';
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
const patch = options.documentAdapter.createPlacedAssetPatch?.({
|
|
1066
|
+
document: beforeDocument,
|
|
1067
|
+
asset: armed.asset,
|
|
1068
|
+
hit,
|
|
1069
|
+
});
|
|
1070
|
+
if (!patch) {
|
|
1071
|
+
state.status = 'Placement rejected: document adapter does not support placed assets';
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
const result = session.dispatch({
|
|
1075
|
+
type: 'document.patch',
|
|
1076
|
+
label: patch.label ?? `Place ${armed.assetId}`,
|
|
1077
|
+
patch: patch.patch,
|
|
1078
|
+
targetId: patch.createdId ?? undefined,
|
|
1079
|
+
});
|
|
1080
|
+
if (!result.documentChanged) {
|
|
1081
|
+
state.status = 'Placement unchanged';
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
const createdId = patch.createdId
|
|
1085
|
+
?? options.documentAdapter.findCreatedId?.(beforeDocument, result.workingDocument)
|
|
1086
|
+
?? null;
|
|
1087
|
+
let selection = result.selection;
|
|
1088
|
+
if (createdId && isNodeSelectableInDocument(options, result.workingDocument, createdId)) {
|
|
1089
|
+
selection = session.dispatch({
|
|
1090
|
+
type: 'selection.replace',
|
|
1091
|
+
selectedIds: [createdId],
|
|
1092
|
+
activeId: createdId,
|
|
1093
|
+
label: 'Select Placed Item',
|
|
1094
|
+
}).selection;
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
selection = sanitizeSelection(state, options, result.workingDocument, selection) ?? selection;
|
|
1098
|
+
}
|
|
1099
|
+
clearArmedPlacement(state);
|
|
1100
|
+
rebuildProjectionFromDocument(state, options, result.workingDocument, selection);
|
|
1101
|
+
state.summary = summarizeDocument(options, result.workingDocument, session.getSource());
|
|
1102
|
+
state.status = patch.label ?? `Placed ${formatAssetLabel(armed.asset, armed.assetId)} at ${formatVec3(hit.position)}`;
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
function pickArmedPlacementHit(state, event) {
|
|
1106
|
+
const mode = state.transformOperationSettings.placementMode;
|
|
1107
|
+
return mode === 'off'
|
|
1108
|
+
? null
|
|
1109
|
+
: state.gizmo?.pickPlacementHit(event.clientX, event.clientY, mode) ?? null;
|
|
1110
|
+
}
|
|
1111
|
+
function clearArmedPlacement(state) {
|
|
1112
|
+
state.armedPlacement = null;
|
|
1113
|
+
state.gizmo?.setPlacementMarker(null);
|
|
1114
|
+
}
|
|
806
1115
|
function addAssetToDocument(state, options, assetId) {
|
|
807
1116
|
if (state.mode !== 'editor')
|
|
808
1117
|
return false;
|
|
@@ -971,6 +1280,86 @@ function findInspectorPropertyByPath(inspector, path) {
|
|
|
971
1280
|
}
|
|
972
1281
|
return null;
|
|
973
1282
|
}
|
|
1283
|
+
function beginDuplicateDrag(state, options, input) {
|
|
1284
|
+
if (state.mode !== 'editor' || !state.session)
|
|
1285
|
+
return null;
|
|
1286
|
+
if (state.duplicateDrag)
|
|
1287
|
+
return null;
|
|
1288
|
+
const document = state.session.getState().workingDocument;
|
|
1289
|
+
const patch = options.documentAdapter.createDuplicateSelectionPatch?.({
|
|
1290
|
+
document,
|
|
1291
|
+
targetIds: input.targetIds,
|
|
1292
|
+
activeId: input.activeId,
|
|
1293
|
+
transforms: input.beforeTransforms,
|
|
1294
|
+
});
|
|
1295
|
+
if (!patch || patch.createdIds.length === 0) {
|
|
1296
|
+
state.status = 'Duplicate drag rejected';
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
const originalSelection = state.session.getSelection();
|
|
1300
|
+
const result = state.session.dispatch({
|
|
1301
|
+
type: 'document.patch',
|
|
1302
|
+
label: patch.label ?? `Duplicate ${input.targetIds.length} object(s)`,
|
|
1303
|
+
patch: patch.patch,
|
|
1304
|
+
targetId: patch.activeId ?? patch.createdIds[patch.createdIds.length - 1] ?? undefined,
|
|
1305
|
+
});
|
|
1306
|
+
if (!result.documentChanged) {
|
|
1307
|
+
state.status = 'Duplicate drag unchanged';
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
const createdIds = patch.createdIds.filter(id => isNodeSelectableInDocument(options, result.workingDocument, id));
|
|
1311
|
+
if (createdIds.length === 0) {
|
|
1312
|
+
const undone = state.session.undo();
|
|
1313
|
+
if (undone)
|
|
1314
|
+
rebuildProjectionFromDocument(state, options, undone.workingDocument, originalSelection);
|
|
1315
|
+
state.status = 'Duplicate drag rejected: duplicated selection is not selectable';
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
const activeId = patch.activeId && createdIds.includes(patch.activeId)
|
|
1319
|
+
? patch.activeId
|
|
1320
|
+
: createdIds[createdIds.length - 1] ?? null;
|
|
1321
|
+
const selectionResult = state.session.dispatch({
|
|
1322
|
+
type: 'selection.replace',
|
|
1323
|
+
selectedIds: createdIds,
|
|
1324
|
+
activeId,
|
|
1325
|
+
label: 'Select Duplicate Drag Targets',
|
|
1326
|
+
});
|
|
1327
|
+
state.duplicateDrag = {
|
|
1328
|
+
originalSelection,
|
|
1329
|
+
createdIds,
|
|
1330
|
+
activeId,
|
|
1331
|
+
};
|
|
1332
|
+
rebuildProjectionFromDocument(state, options, result.workingDocument, selectionResult.selection);
|
|
1333
|
+
if (patch.reprojectIds?.length)
|
|
1334
|
+
reprojectProjectionForChangedIds(state, options, result.workingDocument, patch.reprojectIds);
|
|
1335
|
+
else
|
|
1336
|
+
syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds ?? createdIds);
|
|
1337
|
+
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
1338
|
+
state.status = patch.label ?? `Duplicated ${createdIds.length} object(s)`;
|
|
1339
|
+
return {
|
|
1340
|
+
targetIds: createdIds,
|
|
1341
|
+
activeId,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function cancelDuplicateDrag(state, options) {
|
|
1345
|
+
const duplicate = state.duplicateDrag;
|
|
1346
|
+
if (!duplicate || !state.session)
|
|
1347
|
+
return false;
|
|
1348
|
+
state.duplicateDrag = null;
|
|
1349
|
+
const undone = state.session.undo();
|
|
1350
|
+
if (!undone)
|
|
1351
|
+
return false;
|
|
1352
|
+
const selectionResult = state.session.dispatch({
|
|
1353
|
+
type: 'selection.replace',
|
|
1354
|
+
selectedIds: duplicate.originalSelection.selectedIds,
|
|
1355
|
+
activeId: duplicate.originalSelection.activeId,
|
|
1356
|
+
label: 'Restore Duplicate Drag Selection',
|
|
1357
|
+
});
|
|
1358
|
+
rebuildProjectionFromDocument(state, options, undone.workingDocument, selectionResult.selection);
|
|
1359
|
+
state.summary = summarizeDocument(options, undone.workingDocument, state.session.getSource());
|
|
1360
|
+
state.status = `Canceled duplicate drag ${duplicate.createdIds.length} object(s)`;
|
|
1361
|
+
return true;
|
|
1362
|
+
}
|
|
974
1363
|
function commitGizmoTransform(state, options, event) {
|
|
975
1364
|
if (state.mode !== 'editor' || !state.session)
|
|
976
1365
|
return false;
|
|
@@ -981,8 +1370,10 @@ function commitGizmoTransform(state, options, event) {
|
|
|
981
1370
|
document,
|
|
982
1371
|
});
|
|
983
1372
|
if (!patch) {
|
|
984
|
-
|
|
985
|
-
|
|
1373
|
+
if (!cancelDuplicateDrag(state, options)) {
|
|
1374
|
+
restoreBatchTransformPreview(state, event.targets);
|
|
1375
|
+
state.status = `Ignored ${event.tool} ${event.targetIds.length} objects`;
|
|
1376
|
+
}
|
|
986
1377
|
return false;
|
|
987
1378
|
}
|
|
988
1379
|
const result = state.session.dispatch({
|
|
@@ -990,11 +1381,14 @@ function commitGizmoTransform(state, options, event) {
|
|
|
990
1381
|
label: patch.label ?? `${event.tool} ${event.targetIds.length} objects`,
|
|
991
1382
|
patch: patch.patch,
|
|
992
1383
|
targetId: event.activeId ?? undefined,
|
|
1384
|
+
}, {
|
|
1385
|
+
mergeWithPrevious: event.targetIds.some(id => state.duplicateDrag?.createdIds.includes(id)) === true,
|
|
993
1386
|
});
|
|
994
1387
|
syncProjectionForDispatchResult(state, options, result);
|
|
995
1388
|
syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds ?? event.targetIds);
|
|
996
1389
|
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
997
1390
|
state.status = patch.label ?? `${event.tool} ${event.targetIds.length} objects`;
|
|
1391
|
+
state.duplicateDrag = null;
|
|
998
1392
|
return result.documentChanged;
|
|
999
1393
|
}
|
|
1000
1394
|
const patch = options.documentAdapter.createTransformPatch?.({
|
|
@@ -1002,12 +1396,15 @@ function commitGizmoTransform(state, options, event) {
|
|
|
1002
1396
|
targetId: event.nodeId,
|
|
1003
1397
|
tool: event.tool,
|
|
1004
1398
|
space: event.space,
|
|
1399
|
+
constraint: event.constraint,
|
|
1005
1400
|
before: event.before,
|
|
1006
1401
|
after: event.after,
|
|
1007
1402
|
});
|
|
1008
1403
|
if (!patch) {
|
|
1009
|
-
state
|
|
1010
|
-
|
|
1404
|
+
if (!cancelDuplicateDrag(state, options)) {
|
|
1405
|
+
state.projection?.setNodeTransformPreview(event.nodeId, event.before);
|
|
1406
|
+
state.status = `Ignored ${event.tool} ${event.nodeId}`;
|
|
1407
|
+
}
|
|
1011
1408
|
return false;
|
|
1012
1409
|
}
|
|
1013
1410
|
const result = state.session.dispatch({
|
|
@@ -1015,6 +1412,8 @@ function commitGizmoTransform(state, options, event) {
|
|
|
1015
1412
|
label: patch.label ?? `${event.tool} ${event.nodeId}`,
|
|
1016
1413
|
patch: patch.patch,
|
|
1017
1414
|
targetId: event.nodeId,
|
|
1415
|
+
}, {
|
|
1416
|
+
mergeWithPrevious: state.duplicateDrag?.createdIds.includes(event.nodeId) === true,
|
|
1018
1417
|
});
|
|
1019
1418
|
if (patch.changedIds)
|
|
1020
1419
|
syncProjectionForChangedIds(state, options, result.workingDocument, patch.changedIds);
|
|
@@ -1022,6 +1421,7 @@ function commitGizmoTransform(state, options, event) {
|
|
|
1022
1421
|
syncProjectionForDispatchResult(state, options, result, patch.changedId ?? event.nodeId);
|
|
1023
1422
|
state.summary = summarizeDocument(options, result.workingDocument, state.session.getSource());
|
|
1024
1423
|
state.status = patch.label ?? `${event.tool} ${event.nodeId}`;
|
|
1424
|
+
state.duplicateDrag = null;
|
|
1025
1425
|
return result.documentChanged;
|
|
1026
1426
|
}
|
|
1027
1427
|
function isTransformBatchCommit(event) {
|
|
@@ -1040,6 +1440,7 @@ function cancelActiveOperation(state) {
|
|
|
1040
1440
|
state.sceneViewInput?.cancelActiveIntent();
|
|
1041
1441
|
state.selectionController?.cancelBoxSelection();
|
|
1042
1442
|
cancelActiveGizmoDrag(state);
|
|
1443
|
+
clearArmedPlacement(state);
|
|
1043
1444
|
}
|
|
1044
1445
|
function focusSelectedProjection(state) {
|
|
1045
1446
|
if (state.mode !== 'editor')
|
|
@@ -1072,6 +1473,17 @@ function formatEditorStatusTime(timestamp) {
|
|
|
1072
1473
|
const pad = (value) => String(value).padStart(2, '0');
|
|
1073
1474
|
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1074
1475
|
}
|
|
1476
|
+
function formatVec3(value) {
|
|
1477
|
+
return `${formatPlacementNumber(value.x)}, ${formatPlacementNumber(value.y)}, ${formatPlacementNumber(value.z)}`;
|
|
1478
|
+
}
|
|
1479
|
+
function formatPlacementNumber(value) {
|
|
1480
|
+
return Number.isFinite(value) ? Number(value.toFixed(3)).toString() : String(value);
|
|
1481
|
+
}
|
|
1482
|
+
function formatAssetLabel(asset, fallback) {
|
|
1483
|
+
const record = isObjectRecord(asset) ? asset : null;
|
|
1484
|
+
const label = record?.label ?? record?.displayName ?? record?.name;
|
|
1485
|
+
return typeof label === 'string' && label.trim() ? label : fallback;
|
|
1486
|
+
}
|
|
1075
1487
|
function undoSessionChange(state, options) {
|
|
1076
1488
|
cancelActiveOperation(state);
|
|
1077
1489
|
const result = state.session?.undo();
|
|
@@ -1083,7 +1495,7 @@ function undoSessionChange(state, options) {
|
|
|
1083
1495
|
return true;
|
|
1084
1496
|
}
|
|
1085
1497
|
function redoSessionChange(state, options) {
|
|
1086
|
-
|
|
1498
|
+
cancelActiveOperation(state);
|
|
1087
1499
|
const result = state.session?.redo();
|
|
1088
1500
|
if (!result)
|
|
1089
1501
|
return false;
|
|
@@ -1188,6 +1600,13 @@ function createUiState(state, options) {
|
|
|
1188
1600
|
dragPhase: state.gizmo?.getState().dragPhase ?? 'idle',
|
|
1189
1601
|
draggingNodeId: state.gizmo?.getState().draggingNodeId ?? null,
|
|
1190
1602
|
},
|
|
1603
|
+
transformOperations: {
|
|
1604
|
+
settings: cloneTransformOperationSettings(state.transformOperationSettings),
|
|
1605
|
+
selectedCount: selectedIds.length,
|
|
1606
|
+
activeId,
|
|
1607
|
+
canAlign: selectedIds.length >= 2 && activeId != null,
|
|
1608
|
+
canDistribute: selectedIds.length >= 3,
|
|
1609
|
+
},
|
|
1191
1610
|
session: sessionState
|
|
1192
1611
|
? {
|
|
1193
1612
|
source: sessionState.source,
|