mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
33 Commits
various-fi
...
move-insta
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3e4826bafc | ||
![]() |
d914f9f5be | ||
![]() |
ab8c90dd41 | ||
![]() |
8882841a8d | ||
![]() |
3d357950f9 | ||
![]() |
ef5d2651c0 | ||
![]() |
1dd4bb9b7a | ||
![]() |
682e6f6b03 | ||
![]() |
b468c28ae8 | ||
![]() |
9fc9452a08 | ||
![]() |
3b176b7152 | ||
![]() |
f1d1a9b66b | ||
![]() |
c0d2f491a8 | ||
![]() |
aeecce2ea8 | ||
![]() |
647ee149ff | ||
![]() |
22313e148a | ||
![]() |
31f2d7ce2e | ||
![]() |
4a0efec6c2 | ||
![]() |
5f555df5c1 | ||
![]() |
53a611a1b9 | ||
![]() |
14511b23af | ||
![]() |
fa2371274d | ||
![]() |
0aea8dfa0f | ||
![]() |
81ca18098d | ||
![]() |
b6e44a022f | ||
![]() |
1a8eee2477 | ||
![]() |
d0ef92da03 | ||
![]() |
9c98cb3b3b | ||
![]() |
3681542056 | ||
![]() |
7c0bf135d7 | ||
![]() |
9a31dd046c | ||
![]() |
74401a1f9c | ||
![]() |
cedc6ea3e9 |
@@ -365,6 +365,12 @@ class GD_CORE_API InitialInstance {
|
||||
* the same initial instance between serialization.
|
||||
*/
|
||||
InitialInstance& ResetPersistentUuid();
|
||||
|
||||
/**
|
||||
* \brief Reset the persistent UUID used to recognize
|
||||
* the same initial instance between serialization.
|
||||
*/
|
||||
const gd::String& GetPersistentUuid() { return persistentUuid; };
|
||||
///@}
|
||||
|
||||
private:
|
||||
|
@@ -51,7 +51,9 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
|
||||
currentValue = element.GetChild("value").GetStringValue();
|
||||
type = element.GetChild("type").GetStringValue();
|
||||
if (type == "Number") {
|
||||
gd::String unitName = element.GetChild("unit").GetStringValue();
|
||||
gd::String unitName = element.HasChild("unit")
|
||||
? element.GetChild("unit").GetStringValue()
|
||||
: "";
|
||||
measurementUnit =
|
||||
gd::MeasurementUnit::HasDefaultMeasurementUnitNamed(unitName)
|
||||
? measurementUnit =
|
||||
|
@@ -30,6 +30,14 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
getObjectPositionFrom3DRendererObject() {
|
||||
return {
|
||||
x: this._threeObject3D.position.x - this._object.getWidth() / 2,
|
||||
y: this._threeObject3D.position.y - this._object.getHeight() / 2,
|
||||
z: this._threeObject3D.position.z - this._object.getDepth() / 2,
|
||||
};
|
||||
}
|
||||
|
||||
updateRotation() {
|
||||
this._threeObject3D.rotation.set(
|
||||
gdjs.toRad(this._object.getRotationX()),
|
||||
|
@@ -320,7 +320,7 @@ namespace gdjs {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
this._renderer.ensureUpToDate();
|
||||
}
|
||||
|
||||
|
@@ -216,17 +216,30 @@ bool ExporterHelper::ExportProjectForPixiPreview(
|
||||
// Strip the project (*after* generating events as the events may use stripped
|
||||
// things (objects groups...))
|
||||
gd::ProjectStripper::StripProjectForExport(exportedProject);
|
||||
exportedProject.SetFirstLayout(options.layoutName);
|
||||
|
||||
previousTime = LogTimeSpent("Data stripping", previousTime);
|
||||
|
||||
// Create the setup options passed to the gdjs.RuntimeGame
|
||||
gd::SerializerElement runtimeGameOptions;
|
||||
runtimeGameOptions.AddChild("isPreview").SetBoolValue(true);
|
||||
if (!options.externalLayoutName.empty()) {
|
||||
runtimeGameOptions.AddChild("injectExternalLayout")
|
||||
.SetValue(options.externalLayoutName);
|
||||
|
||||
auto &initialRuntimeGameStatus =
|
||||
runtimeGameOptions.AddChild("initialRuntimeGameStatus");
|
||||
initialRuntimeGameStatus.AddChild("sceneName")
|
||||
.SetStringValue(options.layoutName);
|
||||
if (options.isInGameEdition) {
|
||||
initialRuntimeGameStatus.AddChild("isInGameEdition").SetBoolValue(true);
|
||||
}
|
||||
if (!options.externalLayoutName.empty()) {
|
||||
initialRuntimeGameStatus.AddChild("injectedExternalLayoutName")
|
||||
.SetValue(options.externalLayoutName);
|
||||
|
||||
if (options.isInGameEdition) {
|
||||
initialRuntimeGameStatus.AddChild("skipCreatingInstancesFromScene")
|
||||
.SetBoolValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
runtimeGameOptions.AddChild("projectDataOnlyExport")
|
||||
.SetBoolValue(options.projectDataOnlyExport);
|
||||
runtimeGameOptions.AddChild("nativeMobileApp")
|
||||
|
@@ -45,6 +45,7 @@ struct PreviewExportOptions {
|
||||
projectDataOnlyExport(false),
|
||||
fullLoadingScreen(false),
|
||||
isDevelopmentEnvironment(false),
|
||||
isInGameEdition(false),
|
||||
nonRuntimeScriptsCacheBurst(0),
|
||||
fallbackAuthorId(""),
|
||||
fallbackAuthorUsername(""),
|
||||
@@ -169,6 +170,14 @@ struct PreviewExportOptions {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Set if the export is made for being edited in the editor.
|
||||
*/
|
||||
PreviewExportOptions &SetIsInGameEdition(bool enable) {
|
||||
isInGameEdition = enable;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief If set to a non zero value, the exported script URLs will have an
|
||||
* extra search parameter added (with the given value) to ensure browser cache
|
||||
@@ -291,6 +300,7 @@ struct PreviewExportOptions {
|
||||
bool projectDataOnlyExport;
|
||||
bool fullLoadingScreen;
|
||||
bool isDevelopmentEnvironment;
|
||||
bool isInGameEdition;
|
||||
unsigned int nonRuntimeScriptsCacheBurst;
|
||||
gd::String electronRemoteRequirePath;
|
||||
gd::String gdevelopResourceToken;
|
||||
|
@@ -245,6 +245,8 @@ namespace gdjs {
|
||||
that.sendRuntimeGameDump();
|
||||
} else if (data.command === 'refresh') {
|
||||
that.sendRuntimeGameDump();
|
||||
} else if (data.command === 'getStatus') {
|
||||
that.sendRuntimeGameStatus();
|
||||
} else if (data.command === 'set') {
|
||||
that.set(data.path, data.newValue);
|
||||
} else if (data.command === 'call') {
|
||||
@@ -260,10 +262,89 @@ namespace gdjs {
|
||||
that.sendProfilerStarted();
|
||||
} else if (data.command === 'profiler.stop') {
|
||||
runtimeGame.stopCurrentSceneProfiler();
|
||||
} else if (data.command === 'instances.updated') {
|
||||
runtimeGame._editor.reloadInstances(data.payload);
|
||||
} else if (data.command === 'hotReload') {
|
||||
that._hotReloader.hotReload().then((logs) => {
|
||||
that.sendHotReloaderLogs(logs);
|
||||
// TODO: if fatal error, should probably reload. The editor should handle this
|
||||
// as it knows the current scene to show.
|
||||
});
|
||||
} else if (data.command === 'switchForInGameEdition') {
|
||||
if (!this._runtimegame.isInGameEdition()) return;
|
||||
|
||||
const sceneName = data.sceneName || null;
|
||||
const externalLayoutName = data.externalLayoutName || null;
|
||||
if (!sceneName) {
|
||||
logger.warn('No scene name specified, switchForInGameEdition aborted');
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeGameOptions = this._runtimegame.getAdditionalOptions();
|
||||
if (runtimeGameOptions.initialRuntimeGameStatus) {
|
||||
// Skip changing the scene if we're already on the state that is being requested.
|
||||
if (
|
||||
runtimeGameOptions.initialRuntimeGameStatus.sceneName ===
|
||||
sceneName &&
|
||||
runtimeGameOptions.initialRuntimeGameStatus
|
||||
.injectedExternalLayoutName === externalLayoutName
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
runtimeGame
|
||||
.getSceneStack()
|
||||
.replace({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
skipCreatingInstancesFromScene: !!externalLayoutName,
|
||||
clear: true,
|
||||
});
|
||||
|
||||
// Update initialRuntimeGameStatus so that a hard reload
|
||||
// will come back to the same state, and so that we can check later
|
||||
// if the game is already on the state that is being requested.
|
||||
runtimeGameOptions.initialRuntimeGameStatus = {
|
||||
isPaused: runtimeGame.isPaused(),
|
||||
isInGameEdition: runtimeGame.isInGameEdition(),
|
||||
sceneName: sceneName,
|
||||
injectedExternalLayoutName: externalLayoutName,
|
||||
skipCreatingInstancesFromScene: !!externalLayoutName,
|
||||
};
|
||||
} else if (data.command === 'updateInstances') {
|
||||
// TODO: do an update/partial hot reload of the instances
|
||||
} else if (data.command === 'hardReload') {
|
||||
// This usually means that the preview was modified so much that an entire reload
|
||||
// is needed, or that the runtime itself could have been modified.
|
||||
try {
|
||||
const reloadUrl = new URL(location.href);
|
||||
|
||||
// Construct the initial status to be restored.
|
||||
const initialRuntimeGameStatus = this._runtimegame.getAdditionalOptions()
|
||||
.initialRuntimeGameStatus;
|
||||
const runtimeGameStatus: RuntimeGameStatus = {
|
||||
isPaused: this._runtimegame.isPaused(),
|
||||
isInGameEdition: this._runtimegame.isInGameEdition(),
|
||||
sceneName: initialRuntimeGameStatus?.sceneName || null,
|
||||
injectedExternalLayoutName:
|
||||
initialRuntimeGameStatus?.injectedExternalLayoutName || null,
|
||||
skipCreatingInstancesFromScene:
|
||||
initialRuntimeGameStatus?.skipCreatingInstancesFromScene || false,
|
||||
};
|
||||
|
||||
reloadUrl.searchParams.set(
|
||||
'runtimeGameStatus',
|
||||
JSON.stringify(runtimeGameStatus)
|
||||
);
|
||||
location.replace(reloadUrl);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Could not reload the game with the new initial status',
|
||||
error
|
||||
);
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
logger.info(
|
||||
'Unknown command "' + data.command + '" received by the debugger.'
|
||||
@@ -434,6 +515,20 @@ namespace gdjs {
|
||||
return true;
|
||||
}
|
||||
|
||||
sendRuntimeGameStatus(): void {
|
||||
const currentScene = this._runtimegame.getSceneStack().getCurrentScene();
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'status',
|
||||
payload: {
|
||||
isPaused: this._runtimegame.isPaused(),
|
||||
isInGameEdition: this._runtimegame.isInGameEdition(),
|
||||
sceneName: currentScene ? currentScene.getName() : null,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump all the relevant data from the {@link RuntimeGame} instance and send it to the server.
|
||||
*/
|
||||
@@ -543,26 +638,23 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the game is paused.
|
||||
*/
|
||||
sendGamePaused(): void {
|
||||
sendInstancesUpdated(
|
||||
objectUpdates: Array<{
|
||||
object: RuntimeObject3D;
|
||||
position: { x: number; y: number; z: number };
|
||||
}>,
|
||||
layoutName: string
|
||||
): void {
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'game.paused',
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the game is resumed.
|
||||
*/
|
||||
sendGameResumed(): void {
|
||||
this._sendMessage(
|
||||
circularSafeStringify({
|
||||
command: 'game.resumed',
|
||||
payload: null,
|
||||
command: 'instances.updated',
|
||||
payload: {
|
||||
layoutName,
|
||||
instances: objectUpdates.map((objectUpdate) => ({
|
||||
persistentUuid: objectUpdate.object.persistentUuid,
|
||||
position: objectUpdate.position,
|
||||
})),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -144,8 +144,9 @@ namespace gdjs {
|
||||
});
|
||||
}
|
||||
|
||||
hotReload(): Promise<HotReloaderLog[]> {
|
||||
async hotReload(): Promise<HotReloaderLog[]> {
|
||||
logger.info('Hot reload started');
|
||||
const wasPaused = this._runtimeGame.isPaused();
|
||||
this._runtimeGame.pause(true);
|
||||
this._logs = [];
|
||||
|
||||
@@ -168,62 +169,59 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
// Reload projectData and runtimeGameOptions stored by convention in data.js:
|
||||
return this._reloadScript('data.js').then(() => {
|
||||
const newProjectData: ProjectData = gdjs.projectData;
|
||||
await this._reloadScript('data.js');
|
||||
|
||||
const newRuntimeGameOptions: RuntimeGameOptions =
|
||||
gdjs.runtimeGameOptions;
|
||||
const newProjectData: ProjectData = gdjs.projectData;
|
||||
|
||||
const newScriptFiles = newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
|
||||
const projectDataOnlyExport = !!newRuntimeGameOptions.projectDataOnlyExport;
|
||||
const newRuntimeGameOptions: RuntimeGameOptions = gdjs.runtimeGameOptions;
|
||||
|
||||
// Reload the changed scripts, which will have the side effects of re-running
|
||||
// the new scripts, potentially replacing the code of the free functions from
|
||||
// extensions (which is fine) and registering updated behaviors (which will
|
||||
// need to be re-instantiated in runtime objects).
|
||||
return this.reloadScriptFiles(
|
||||
const newScriptFiles = newRuntimeGameOptions.scriptFiles as RuntimeGameOptionsScriptFile[];
|
||||
const projectDataOnlyExport = !!newRuntimeGameOptions.projectDataOnlyExport;
|
||||
|
||||
// Reload the changed scripts, which will have the side effects of re-running
|
||||
// the new scripts, potentially replacing the code of the free functions from
|
||||
// extensions (which is fine) and registering updated behaviors (which will
|
||||
// need to be re-instantiated in runtime objects).
|
||||
try {
|
||||
await this.reloadScriptFiles(
|
||||
newProjectData,
|
||||
oldScriptFiles,
|
||||
newScriptFiles,
|
||||
projectDataOnlyExport
|
||||
)
|
||||
.then(() => {
|
||||
const changedRuntimeBehaviors = this._computeChangedRuntimeBehaviors(
|
||||
oldBehaviorConstructors,
|
||||
gdjs.behaviorsTypes.items
|
||||
);
|
||||
return this._hotReloadRuntimeGame(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
this._runtimeGame
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorTarget = error.target;
|
||||
if (errorTarget instanceof HTMLScriptElement) {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message: 'Unable to reload script: ' + errorTarget.src,
|
||||
});
|
||||
} else {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message:
|
||||
'Unexpected error happened while hot-reloading: ' +
|
||||
error.message,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(
|
||||
'Hot reload finished with logs:',
|
||||
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
|
||||
);
|
||||
this._runtimeGame.pause(false);
|
||||
return this._logs;
|
||||
);
|
||||
|
||||
const changedRuntimeBehaviors = this._computeChangedRuntimeBehaviors(
|
||||
oldBehaviorConstructors,
|
||||
gdjs.behaviorsTypes.items
|
||||
);
|
||||
await this._hotReloadRuntimeGame(
|
||||
oldProjectData,
|
||||
newProjectData,
|
||||
changedRuntimeBehaviors,
|
||||
this._runtimeGame
|
||||
);
|
||||
} catch (error) {
|
||||
const errorTarget = error.target;
|
||||
if (errorTarget instanceof HTMLScriptElement) {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message: 'Unable to reload script: ' + errorTarget.src,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this._logs.push({
|
||||
kind: 'fatal',
|
||||
message:
|
||||
'Unexpected error happened while hot-reloading: ' + error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'Hot reload finished with logs:',
|
||||
this._logs.map((log) => '\n' + log.kind + ': ' + log.message)
|
||||
);
|
||||
this._runtimeGame.pause(wasPaused);
|
||||
return this._logs;
|
||||
}
|
||||
|
||||
_computeChangedRuntimeBehaviors(
|
||||
|
File diff suppressed because one or more lines are too long
@@ -41,14 +41,54 @@ namespace gdjs {
|
||||
return supportedCompressionMethods;
|
||||
};
|
||||
|
||||
/**
|
||||
* The desired status of the game, used for previews or in-game edition.
|
||||
* Either stored in the options generated by the preview or in the URL
|
||||
* in case of a hard reload.
|
||||
*/
|
||||
export type RuntimeGameStatus = {
|
||||
isPaused: boolean;
|
||||
isInGameEdition: boolean;
|
||||
sceneName: string | null;
|
||||
injectedExternalLayoutName: string | null;
|
||||
skipCreatingInstancesFromScene: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the desired status of the game from the URL. Only useful for previews
|
||||
* when hard reloaded.
|
||||
*/
|
||||
const readRuntimeGameStatusFromUrl = (): RuntimeGameStatus | null => {
|
||||
try {
|
||||
const url = new URL(location.href);
|
||||
const runtimeGameStatus = url.searchParams.get('runtimeGameStatus');
|
||||
if (!runtimeGameStatus) return null;
|
||||
|
||||
const parsedRuntimeGameStatus = JSON.parse(runtimeGameStatus);
|
||||
return {
|
||||
isPaused: !!parsedRuntimeGameStatus.isPaused,
|
||||
isInGameEdition: !!parsedRuntimeGameStatus.isInGameEdition,
|
||||
sceneName: '' + parsedRuntimeGameStatus.sceneName,
|
||||
injectedExternalLayoutName:
|
||||
'' + parsedRuntimeGameStatus.injectedExternalLayoutName,
|
||||
skipCreatingInstancesFromScene: !!parsedRuntimeGameStatus.skipCreatingInstancesFromScene,
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/** Options given to the game at startup. */
|
||||
export type RuntimeGameOptions = {
|
||||
/** if true, force fullscreen. */
|
||||
forceFullscreen?: boolean;
|
||||
|
||||
/** if true, game is run as a preview launched from an editor. */
|
||||
isPreview?: boolean;
|
||||
/** The name of the external layout to create in the scene at position 0;0. */
|
||||
injectExternalLayout?: string;
|
||||
|
||||
/** if set, the status of the game to be restored. */
|
||||
initialRuntimeGameStatus?: RuntimeGameStatus;
|
||||
|
||||
/** Script files, used for hot-reloading. */
|
||||
scriptFiles?: Array<RuntimeGameOptionsScriptFile>;
|
||||
/** if true, export is a partial preview without events. */
|
||||
@@ -118,6 +158,239 @@ namespace gdjs {
|
||||
environment?: 'dev';
|
||||
};
|
||||
|
||||
class RuntimeEditor {
|
||||
_game: RuntimeGame;
|
||||
_pointer = new THREE.Vector2();
|
||||
// TODO: Use array
|
||||
_selectedObjectData: {
|
||||
intersect: THREE.Intersection;
|
||||
camera: THREE.Camera;
|
||||
scene: THREE.Scene;
|
||||
} | null = null;
|
||||
_outlinePasses: Record<string, THREE_ADDONS.OutlinePass> = {};
|
||||
_raycaster = new THREE.Raycaster()
|
||||
_editionAbortController: AbortController = new AbortController();
|
||||
_clearCurrentPasses: (() => void)[] = [];
|
||||
_currentTransformControls: THREE_ADDONS.TransformControls | null = null;
|
||||
_shouldIgnoreNextClick: boolean = false;
|
||||
|
||||
constructor(game: RuntimeGame) {
|
||||
this._game = game;
|
||||
}
|
||||
|
||||
onPointerMove(event) {
|
||||
// calculate pointer position in normalized device coordinates
|
||||
// (-1 to +1) for both components
|
||||
|
||||
this._pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
this._pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
}
|
||||
|
||||
selectObject() {
|
||||
if (this._shouldIgnoreNextClick) {
|
||||
this._shouldIgnoreNextClick = false;
|
||||
return;
|
||||
}
|
||||
const firstIntersectsByLayer = this.getFirstIntersectsOnEachLayer(false);
|
||||
if (!firstIntersectsByLayer) return;
|
||||
let closestIntersect;
|
||||
for (const intersect of Object.values(firstIntersectsByLayer)) {
|
||||
if (
|
||||
!closestIntersect ||
|
||||
intersect.intersect.distance < closestIntersect.intersect.distance
|
||||
) {
|
||||
closestIntersect = intersect;
|
||||
}
|
||||
}
|
||||
this._selectedObjectData = closestIntersect;
|
||||
if (!this._selectedObjectData) return;
|
||||
|
||||
if (
|
||||
this._currentTransformControls &&
|
||||
this._currentTransformControls.camera ===
|
||||
this._selectedObjectData.camera
|
||||
) {
|
||||
this._currentTransformControls.detach();
|
||||
this._currentTransformControls.attach(
|
||||
this._selectedObjectData.intersect.object
|
||||
);
|
||||
} else {
|
||||
if (this._currentTransformControls) {
|
||||
this._currentTransformControls.detach();
|
||||
this._currentTransformControls = null;
|
||||
}
|
||||
this._currentTransformControls = new THREE_ADDONS.TransformControls(
|
||||
this._selectedObjectData.camera,
|
||||
this._game.getRenderer().getCanvas() || undefined
|
||||
);
|
||||
this._currentTransformControls.addEventListener('mouseDown', () => {
|
||||
this._shouldIgnoreNextClick = true;
|
||||
});
|
||||
this._currentTransformControls.addEventListener(
|
||||
'dragging-changed',
|
||||
(e) => {
|
||||
if (!this._selectedObjectData) return;
|
||||
if (e.value) {
|
||||
// Ignore if the user starts dragging
|
||||
return;
|
||||
}
|
||||
const rendererObject = this._selectedObjectData.intersect.object;
|
||||
const object = this._game.getObjectFromRenderer(rendererObject);
|
||||
if (!object) return;
|
||||
if (object instanceof gdjs.RuntimeObject3D) {
|
||||
this._game.sendRuntimeObjectsUpdated([
|
||||
{
|
||||
object,
|
||||
position: object
|
||||
.getRenderer()
|
||||
.getObjectPositionFrom3DRendererObject(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
);
|
||||
this._currentTransformControls.scale.y = -1;
|
||||
this._currentTransformControls.attach(
|
||||
this._selectedObjectData.intersect.object
|
||||
);
|
||||
this._selectedObjectData.scene.add(this._currentTransformControls);
|
||||
}
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
const canvas = this._game.getRenderer().getCanvas();
|
||||
this._editionAbortController = new AbortController();
|
||||
canvas?.addEventListener('pointermove', this.onPointerMove.bind(this), {
|
||||
signal: this._editionAbortController.signal,
|
||||
});
|
||||
canvas?.addEventListener('click', this.selectObject.bind(this), {
|
||||
signal: this._editionAbortController.signal,
|
||||
});
|
||||
}
|
||||
cleanListeners() {
|
||||
this._editionAbortController.abort();
|
||||
}
|
||||
activate(enable: boolean) {
|
||||
if (enable) this.setupListeners();
|
||||
else {
|
||||
this.cleanListeners();
|
||||
if (this._currentTransformControls) {
|
||||
this._currentTransformControls.detach();
|
||||
this._currentTransformControls = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reloadInstances(payload: {
|
||||
layoutName: string;
|
||||
instances: Array<{
|
||||
persistentUuid: string;
|
||||
position: { x: number; y: number; z: number };
|
||||
}>;
|
||||
}) {
|
||||
const currentScene = this._game.getSceneStack().getCurrentScene();
|
||||
if (!currentScene || currentScene.getName() !== payload.layoutName) {
|
||||
return;
|
||||
}
|
||||
// TODO: Might be worth indexing instances data and runtime objects by their
|
||||
// persistentUuid (See HotReloader.indexByPersistentUuid).
|
||||
currentScene.getAdhocListOfAllInstances().forEach((runtimeObject) => {
|
||||
const instance = payload.instances.find(
|
||||
(instance) => instance.persistentUuid === runtimeObject.persistentUuid
|
||||
);
|
||||
if (instance) {
|
||||
runtimeObject.setX(instance.position.x);
|
||||
runtimeObject.setY(instance.position.y);
|
||||
if (runtimeObject instanceof gdjs.RuntimeObject3D) {
|
||||
runtimeObject.setZ(instance.position.z);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getFirstIntersectsOnEachLayer(highlightObject: boolean) {
|
||||
if (highlightObject) {
|
||||
this._clearCurrentPasses.forEach((callback) => callback());
|
||||
this._clearCurrentPasses.length = 0;
|
||||
}
|
||||
const layerNames = new Array();
|
||||
const currentScene = this._game.getSceneStack().getCurrentScene();
|
||||
if (!currentScene) return;
|
||||
const threeRenderer = this._game.getRenderer().getThreeRenderer();
|
||||
if (!threeRenderer) return;
|
||||
|
||||
currentScene.getAllLayerNames(layerNames);
|
||||
const firstIntersectsByLayer: {
|
||||
[layerName: string]: {
|
||||
intersect: THREE.Intersection;
|
||||
camera: THREE.Camera;
|
||||
scene: THREE.Scene;
|
||||
};
|
||||
} = layerNames.reduce((acc, layerName) => {
|
||||
const runtimeLayerRender = currentScene
|
||||
.getLayer(layerName)
|
||||
.getRenderer();
|
||||
const threeCamera = runtimeLayerRender.getThreeCamera();
|
||||
const threeScene = runtimeLayerRender.getThreeScene();
|
||||
if (!threeCamera || !threeScene) return acc;
|
||||
|
||||
if (!this._outlinePasses[layerName]) {
|
||||
this._outlinePasses[layerName] = new THREE_ADDONS.OutlinePass(
|
||||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||
threeScene,
|
||||
threeCamera
|
||||
);
|
||||
|
||||
runtimeLayerRender.addPostProcessingPass(this._outlinePasses[layerName]);
|
||||
}
|
||||
const outlinePass = this._outlinePasses[layerName];
|
||||
|
||||
// Note that raycasting is done by Three.js, which means it could slow down
|
||||
// if lots of 3D objects are shown. We consider that if this needs improvements,
|
||||
// this must be handled by the game engine culling
|
||||
this._raycaster.setFromCamera(this._pointer, threeCamera);
|
||||
const intersects = this._raycaster.intersectObjects(threeScene.children);
|
||||
|
||||
const firstIntersect = intersects.filter((intersect) => {
|
||||
let isObjectChildOfTransformControls = false;
|
||||
intersect.object.traverseAncestors((ancestor) => {
|
||||
if (ancestor === this._currentTransformControls) {
|
||||
isObjectChildOfTransformControls = true;
|
||||
}
|
||||
});
|
||||
return !isObjectChildOfTransformControls;
|
||||
})[0];
|
||||
|
||||
if (
|
||||
firstIntersect &&
|
||||
highlightObject &&
|
||||
(!this._currentTransformControls ||
|
||||
!this._currentTransformControls.dragging)
|
||||
) {
|
||||
// TODO: OutlinePass currently wrongly highlights the transform controls helper.
|
||||
// (See https://discourse.threejs.org/t/outlinepass-with-transform-control/18722)
|
||||
|
||||
outlinePass.edgeStrength = 6.0;
|
||||
outlinePass.edgeGlow = 0;
|
||||
outlinePass.edgeThickness = 1.0;
|
||||
outlinePass.pulsePeriod = 0;
|
||||
outlinePass.selectedObjects = [firstIntersect.object];
|
||||
}
|
||||
acc[layerName] = {
|
||||
intersect: firstIntersect,
|
||||
camera: threeCamera,
|
||||
scene: threeScene,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
return firstIntersectsByLayer;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.getFirstIntersectsOnEachLayer(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a game being played.
|
||||
*/
|
||||
@@ -169,10 +442,6 @@ namespace gdjs {
|
||||
//Inputs :
|
||||
_inputManager: InputManager;
|
||||
|
||||
/**
|
||||
* Allow to specify an external layout to insert in the first scene.
|
||||
*/
|
||||
_injectExternalLayout: any;
|
||||
_options: RuntimeGameOptions;
|
||||
|
||||
/**
|
||||
@@ -187,6 +456,7 @@ namespace gdjs {
|
||||
_sessionMetricsInitialized: boolean = false;
|
||||
_disableMetrics: boolean = false;
|
||||
_isPreview: boolean;
|
||||
_isInGameEdition: boolean;
|
||||
|
||||
/**
|
||||
* The capture manager, used to manage captures (screenshots, videos, etc...).
|
||||
@@ -196,12 +466,27 @@ namespace gdjs {
|
||||
/** True if the RuntimeGame has been disposed and should not be used anymore. */
|
||||
_wasDisposed: boolean = false;
|
||||
|
||||
_editor: RuntimeEditor;
|
||||
|
||||
/**
|
||||
* @param data The object (usually stored in data.json) containing the full project data
|
||||
* @param
|
||||
*/
|
||||
constructor(data: ProjectData, options?: RuntimeGameOptions) {
|
||||
this._options = options || {};
|
||||
|
||||
this._isPreview = this._options.isPreview || false;
|
||||
if (this._isPreview) {
|
||||
// Check if we need to restore the state from the URL, which is used
|
||||
// when a preview is hard reloaded (search for `hardReload`).
|
||||
const runtimeGameStatusFromUrl = readRuntimeGameStatusFromUrl();
|
||||
if (runtimeGameStatusFromUrl) {
|
||||
this._options.initialRuntimeGameStatus = runtimeGameStatusFromUrl;
|
||||
}
|
||||
}
|
||||
this._isInGameEdition =
|
||||
this._options.initialRuntimeGameStatus?.isInGameEdition || false;
|
||||
|
||||
this._variables = new gdjs.VariablesContainer(data.variables);
|
||||
this._variablesByExtensionName = new Map<
|
||||
string,
|
||||
@@ -225,7 +510,7 @@ namespace gdjs {
|
||||
getGlobalResourceNames(data),
|
||||
data.layouts
|
||||
);
|
||||
|
||||
this._editor = new RuntimeEditor(this);
|
||||
this._effectsManager = new gdjs.EffectsManager();
|
||||
this._maxFPS = this._data.properties.maxFPS;
|
||||
this._minFPS = this._data.properties.minFPS;
|
||||
@@ -250,7 +535,6 @@ namespace gdjs {
|
||||
);
|
||||
this._sceneStack = new gdjs.SceneStack(this);
|
||||
this._inputManager = new gdjs.InputManager();
|
||||
this._injectExternalLayout = this._options.injectExternalLayout || '';
|
||||
this._debuggerClient = gdjs.DebuggerClient
|
||||
? new gdjs.DebuggerClient(this)
|
||||
: null;
|
||||
@@ -260,7 +544,6 @@ namespace gdjs {
|
||||
this._options.captureOptions || {}
|
||||
)
|
||||
: null;
|
||||
this._isPreview = this._options.isPreview || false;
|
||||
this._sessionId = null;
|
||||
this._playerId = null;
|
||||
|
||||
@@ -709,9 +992,9 @@ namespace gdjs {
|
||||
if (this._paused === enable) return;
|
||||
|
||||
this._paused = enable;
|
||||
this._editor.activate(enable);
|
||||
if (this._debuggerClient) {
|
||||
if (this._paused) this._debuggerClient.sendGamePaused();
|
||||
else this._debuggerClient.sendGameResumed();
|
||||
this._debuggerClient.sendRuntimeGameStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,11 +1155,16 @@ namespace gdjs {
|
||||
await loadAssets(onProgress);
|
||||
|
||||
await loadingScreen.unload();
|
||||
this.pause(false);
|
||||
|
||||
if (!this._isInGameEdition) {
|
||||
this.pause(false);
|
||||
}
|
||||
}
|
||||
|
||||
private _getFirstSceneName(): string {
|
||||
const firstSceneName = this._data.firstLayout;
|
||||
const firstSceneName =
|
||||
this._options.initialRuntimeGameStatus?.sceneName ||
|
||||
this._data.firstLayout;
|
||||
return this.hasScene(firstSceneName)
|
||||
? firstSceneName
|
||||
: // There is always at least a scene
|
||||
@@ -896,10 +1184,15 @@ namespace gdjs {
|
||||
this._forceGameResolutionUpdate();
|
||||
|
||||
// Load the first scene
|
||||
this._sceneStack.push(
|
||||
this._getFirstSceneName(),
|
||||
this._injectExternalLayout
|
||||
);
|
||||
this._sceneStack.push({
|
||||
sceneName: this._getFirstSceneName(),
|
||||
externalLayoutName:
|
||||
this._options.initialRuntimeGameStatus
|
||||
?.injectedExternalLayoutName || undefined,
|
||||
skipCreatingInstancesFromScene:
|
||||
this._options.initialRuntimeGameStatus
|
||||
?.skipCreatingInstancesFromScene || false,
|
||||
});
|
||||
this._watermark.displayAtStartup();
|
||||
|
||||
//Uncomment to profile the first x frames of the game.
|
||||
@@ -917,11 +1210,29 @@ namespace gdjs {
|
||||
this._setupGameVisibilityEvents();
|
||||
|
||||
// The standard game loop
|
||||
let lastFrameSceneName: string | null = null;
|
||||
let accumulatedElapsedTime = 0;
|
||||
this._hasJustResumed = false;
|
||||
this._renderer.startGameLoop((lastCallElapsedTime) => {
|
||||
try {
|
||||
if (this._debuggerClient) {
|
||||
// Watch the scene name to automatically update debugger when a scene is changed.
|
||||
const currentScene = this.getSceneStack().getCurrentScene();
|
||||
if (
|
||||
currentScene &&
|
||||
currentScene.getName() !== lastFrameSceneName
|
||||
) {
|
||||
lastFrameSceneName = currentScene.getName();
|
||||
this._debuggerClient.sendRuntimeGameStatus();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._paused) {
|
||||
this._editor.render();
|
||||
// The game is paused for edition: the game loop continues to run,
|
||||
// but the game logic is not executed.
|
||||
this._sceneStack.renderWithoutStep();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1273,6 +1584,22 @@ namespace gdjs {
|
||||
return this._isPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game loop is paused, for debugging/edition purposes.
|
||||
* @returns true if the current game is paused
|
||||
*/
|
||||
isPaused(): boolean {
|
||||
return this._paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should display in-game edition tools or not.
|
||||
* @returns true if the current game is being edited.
|
||||
*/
|
||||
isInGameEdition(): boolean {
|
||||
return this._isInGameEdition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game should call GDevelop development APIs or not.
|
||||
*
|
||||
@@ -1319,6 +1646,38 @@ namespace gdjs {
|
||||
: embeddedResourceName;
|
||||
}
|
||||
|
||||
getObjectFromRenderer(
|
||||
renderer: THREE.Object3D | gdjs.RendererObjectInterface
|
||||
): RuntimeObject | null {
|
||||
const currentScene = this.getSceneStack().getCurrentScene();
|
||||
if (!currentScene) return null;
|
||||
// TODO: Should we instead have an index on the runtime object persistentUuid?
|
||||
// In that case, it would require to store the persistentUuid on the renderer object.
|
||||
const allInstances = currentScene.getAdhocListOfAllInstances();
|
||||
const object = allInstances.find(
|
||||
renderer instanceof THREE.Object3D
|
||||
? (instance) => instance.get3DRendererObject() === renderer
|
||||
: (instance) => instance.getRendererObject() === renderer
|
||||
);
|
||||
|
||||
return object || null;
|
||||
}
|
||||
|
||||
sendRuntimeObjectsUpdated(
|
||||
runtimeObjectUpdates: Array<{
|
||||
object: RuntimeObject3D;
|
||||
position: { x: number; y: number; z: number };
|
||||
}>
|
||||
) {
|
||||
if (!this._debuggerClient) return;
|
||||
const currentScene = this.getSceneStack().getCurrentScene();
|
||||
if (!currentScene) return;
|
||||
this._debuggerClient.sendInstancesUpdated(
|
||||
runtimeObjectUpdates,
|
||||
currentScene.getName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of resources that are embedded to passed one.
|
||||
* @param resourceName The name of resource to find embedded resources of.
|
||||
|
@@ -121,10 +121,15 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Load the runtime scene from the given scene.
|
||||
* @param sceneData An object containing the scene data.
|
||||
*
|
||||
* @param sceneAndExtensionsData The data of the scene and extension variables to be loaded.
|
||||
* @param options Options to change what is loaded.
|
||||
* @see gdjs.RuntimeGame#getSceneAndExtensionsData
|
||||
*/
|
||||
loadFromScene(sceneAndExtensionsData: SceneAndExtensionsData | null) {
|
||||
loadFromScene(
|
||||
sceneAndExtensionsData: SceneAndExtensionsData | null,
|
||||
options?: { skipCreatingInstances?: boolean }
|
||||
) {
|
||||
if (!sceneAndExtensionsData) {
|
||||
logger.error('loadFromScene was called without a scene');
|
||||
return;
|
||||
@@ -184,14 +189,16 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
//Create initial instances of objects
|
||||
this.createObjectsFrom(
|
||||
sceneData.instances,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
/*trackByPersistentUuid=*/
|
||||
true
|
||||
);
|
||||
if (!options || !options.skipCreatingInstances) {
|
||||
this.createObjectsFrom(
|
||||
sceneData.instances,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
/*trackByPersistentUuid=*/
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Set up the default z order (for objects created from events)
|
||||
this._setLayerDefaultZOrders();
|
||||
@@ -358,7 +365,7 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Step and render the scene.
|
||||
* Step (execute the game logic) and render the scene.
|
||||
* @param elapsedTime In milliseconds
|
||||
* @return true if the game loop should continue, false if a scene change/push/pop
|
||||
* or a game stop was requested.
|
||||
@@ -418,6 +425,21 @@ namespace gdjs {
|
||||
if (this._profiler) {
|
||||
this._profiler.end('callbacks and extensions (post-events)');
|
||||
}
|
||||
|
||||
this.render();
|
||||
this._isJustResumed = false;
|
||||
if (this._profiler) {
|
||||
this._profiler.end('render');
|
||||
}
|
||||
if (this._profiler) {
|
||||
this._profiler.endFrame();
|
||||
}
|
||||
return !!this.getRequestedChange();
|
||||
}
|
||||
/**
|
||||
* Render the scene (but do not execute the game logic).
|
||||
*/
|
||||
render() {
|
||||
if (this._profiler) {
|
||||
this._profiler.begin('objects (pre-render, effects update)');
|
||||
}
|
||||
@@ -447,21 +469,6 @@ namespace gdjs {
|
||||
);
|
||||
}
|
||||
|
||||
this._isJustResumed = false;
|
||||
this.render();
|
||||
if (this._profiler) {
|
||||
this._profiler.end('render');
|
||||
}
|
||||
if (this._profiler) {
|
||||
this._profiler.endFrame();
|
||||
}
|
||||
return !!this.getRequestedChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the PIXI container associated to the runtimeScene.
|
||||
*/
|
||||
render() {
|
||||
this._renderer.render();
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,16 @@ namespace gdjs {
|
||||
const logger = new gdjs.Logger('Scene stack');
|
||||
const debugLogger = new gdjs.Logger('Multiplayer - Debug');
|
||||
|
||||
interface PushSceneOptions {
|
||||
sceneName: string;
|
||||
externalLayoutName?: string;
|
||||
skipCreatingInstancesFromScene?: boolean;
|
||||
};
|
||||
|
||||
interface ReplaceSceneOptions extends PushSceneOptions {
|
||||
clear: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hold the stack of scenes ({@link gdjs.RuntimeScene}) being played.
|
||||
*/
|
||||
@@ -113,15 +123,28 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pause the scene currently being played and start the new scene that is specified.
|
||||
* If `externalLayoutName` is set, also instantiate the objects from this external layout.
|
||||
* Pause the scene currently being played and start the new scene that is specified in `options.sceneName`.
|
||||
* If `options.externalLayoutName` is set, also instantiate the objects from this external layout.
|
||||
*
|
||||
* @param options Contains the scene name and optional external layout name to instantiate.
|
||||
* @param deprecatedExternalLayoutName Deprecated, use `options.externalLayoutName` instead.
|
||||
*/
|
||||
push(
|
||||
newSceneName: string,
|
||||
externalLayoutName?: string
|
||||
options: PushSceneOptions | string,
|
||||
deprecatedExternalLayoutName?: string
|
||||
): gdjs.RuntimeScene | null {
|
||||
this._throwIfDisposed();
|
||||
console.log({options, deprecatedExternalLayoutName})
|
||||
|
||||
const sceneName =
|
||||
typeof options === 'string' ? options : options.sceneName;
|
||||
const skipCreatingInstancesFromScene =
|
||||
typeof options === 'string' ? false : options.skipCreatingInstancesFromScene;
|
||||
const externalLayoutName =
|
||||
deprecatedExternalLayoutName ||
|
||||
(typeof options === 'string' ? undefined : options.externalLayoutName);
|
||||
|
||||
// Tell the scene it's being paused
|
||||
const currentScene = this._stack[this._stack.length - 1];
|
||||
@@ -131,35 +154,43 @@ namespace gdjs {
|
||||
|
||||
// Avoid a risk of displaying an intermediate loading screen
|
||||
// during 1 frame.
|
||||
if (this._runtimeGame.areSceneAssetsReady(newSceneName)) {
|
||||
return this._loadNewScene(newSceneName, externalLayoutName);
|
||||
if (this._runtimeGame.areSceneAssetsReady(sceneName)) {
|
||||
return this._loadNewScene({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
skipCreatingInstancesFromScene,
|
||||
});
|
||||
}
|
||||
|
||||
this._isNextLayoutLoading = true;
|
||||
this._runtimeGame.loadSceneAssets(newSceneName).then(() => {
|
||||
this._loadNewScene(newSceneName);
|
||||
this._runtimeGame.loadSceneAssets(sceneName).then(() => {
|
||||
this._loadNewScene({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
skipCreatingInstancesFromScene,
|
||||
});
|
||||
this._isNextLayoutLoading = false;
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
private _loadNewScene(
|
||||
newSceneName: string,
|
||||
externalLayoutName?: string
|
||||
): gdjs.RuntimeScene {
|
||||
private _loadNewScene(options: PushSceneOptions): gdjs.RuntimeScene {
|
||||
this._throwIfDisposed();
|
||||
|
||||
// Load the new one
|
||||
const newScene = new gdjs.RuntimeScene(this._runtimeGame);
|
||||
newScene.loadFromScene(
|
||||
this._runtimeGame.getSceneAndExtensionsData(newSceneName)
|
||||
this._runtimeGame.getSceneAndExtensionsData(options.sceneName),
|
||||
{
|
||||
skipCreatingInstances: options.skipCreatingInstancesFromScene,
|
||||
}
|
||||
);
|
||||
this._wasFirstSceneLoaded = true;
|
||||
|
||||
// Optionally create the objects from an external layout.
|
||||
if (externalLayoutName) {
|
||||
if (options.externalLayoutName) {
|
||||
const externalLayoutData = this._runtimeGame.getExternalLayoutData(
|
||||
externalLayoutName
|
||||
options.externalLayoutName
|
||||
);
|
||||
if (externalLayoutData) {
|
||||
newScene.createObjectsFrom(
|
||||
@@ -177,10 +208,15 @@ namespace gdjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the specified scene, replacing the one currently being played.
|
||||
* If `clear` is set to true, all running scenes are also removed from the stack of scenes.
|
||||
* Start the scene in `options.sceneName`, replacing the one currently being played.
|
||||
* If `options.clear` is set to true, all running scenes are also removed from the stack of scenes.
|
||||
*
|
||||
* @param options Contains the scene name and optional external layout name to instantiate.
|
||||
* @param deprecatedClear Deprecated, use `options.clear` instead.
|
||||
*/
|
||||
replace(newSceneName: string, clear?: boolean): gdjs.RuntimeScene | null {
|
||||
replace(options: ReplaceSceneOptions | string, deprecatedClear?: boolean): gdjs.RuntimeScene | null {
|
||||
const clear = deprecatedClear || typeof options === 'string' ? false : options.clear;
|
||||
|
||||
this._throwIfDisposed();
|
||||
if (!!clear) {
|
||||
// Unload all the scenes
|
||||
@@ -199,7 +235,7 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.push(newSceneName);
|
||||
return this.push(options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
4
GDJS/Runtime/types/global-three-addons.d.ts
vendored
4
GDJS/Runtime/types/global-three-addons.d.ts
vendored
@@ -2,7 +2,9 @@ import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
|
||||
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
|
||||
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
|
||||
import { Pass } from 'three/examples/jsm/postprocessing/Pass';
|
||||
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
|
||||
@@ -21,7 +23,9 @@ declare global {
|
||||
GLTF,
|
||||
DRACOLoader,
|
||||
SkeletonUtils,
|
||||
TransformControls,
|
||||
EffectComposer,
|
||||
OutlinePass,
|
||||
Pass,
|
||||
RenderPass,
|
||||
ShaderPass,
|
||||
|
@@ -1373,6 +1373,7 @@ interface InitialInstance {
|
||||
double GetCustomDepth();
|
||||
|
||||
[Ref] InitialInstance ResetPersistentUuid();
|
||||
[Const, Value] DOMString GetPersistentUuid();
|
||||
|
||||
void UpdateCustomProperty(
|
||||
[Const] DOMString name,
|
||||
@@ -3843,6 +3844,7 @@ interface PreviewExportOptions {
|
||||
[Ref] PreviewExportOptions SetNativeMobileApp(boolean enable);
|
||||
[Ref] PreviewExportOptions SetFullLoadingScreen(boolean enable);
|
||||
[Ref] PreviewExportOptions SetIsDevelopmentEnvironment(boolean enable);
|
||||
[Ref] PreviewExportOptions SetIsInGameEdition(boolean enable);
|
||||
[Ref] PreviewExportOptions SetNonRuntimeScriptsCacheBurst(unsigned long value);
|
||||
[Ref] PreviewExportOptions SetElectronRemoteRequirePath([Const] DOMString electronRemoteRequirePath);
|
||||
[Ref] PreviewExportOptions SetGDevelopResourceToken([Const] DOMString gdevelopResourceToken);
|
||||
|
2
GDevelop.js/types.d.ts
vendored
2
GDevelop.js/types.d.ts
vendored
@@ -1145,6 +1145,7 @@ export class InitialInstance extends EmscriptenObject {
|
||||
setCustomDepth(depth: number): void;
|
||||
getCustomDepth(): number;
|
||||
resetPersistentUuid(): InitialInstance;
|
||||
getPersistentUuid(): string;
|
||||
updateCustomProperty(name: string, value: string, globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): void;
|
||||
getCustomProperties(globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer): MapStringPropertyDescriptor;
|
||||
getRawDoubleProperty(name: string): number;
|
||||
@@ -2850,6 +2851,7 @@ export class PreviewExportOptions extends EmscriptenObject {
|
||||
setNativeMobileApp(enable: boolean): PreviewExportOptions;
|
||||
setFullLoadingScreen(enable: boolean): PreviewExportOptions;
|
||||
setIsDevelopmentEnvironment(enable: boolean): PreviewExportOptions;
|
||||
setIsInGameEdition(enable: boolean): PreviewExportOptions;
|
||||
setNonRuntimeScriptsCacheBurst(value: number): PreviewExportOptions;
|
||||
setElectronRemoteRequirePath(electronRemoteRequirePath: string): PreviewExportOptions;
|
||||
setGDevelopResourceToken(gdevelopResourceToken: string): PreviewExportOptions;
|
||||
|
@@ -44,6 +44,7 @@ declare class gdInitialInstance {
|
||||
setCustomDepth(depth: number): void;
|
||||
getCustomDepth(): number;
|
||||
resetPersistentUuid(): gdInitialInstance;
|
||||
getPersistentUuid(): string;
|
||||
updateCustomProperty(name: string, value: string, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): void;
|
||||
getCustomProperties(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer): gdMapStringPropertyDescriptor;
|
||||
getRawDoubleProperty(name: string): number;
|
||||
|
@@ -13,6 +13,7 @@ declare class gdPreviewExportOptions {
|
||||
setNativeMobileApp(enable: boolean): gdPreviewExportOptions;
|
||||
setFullLoadingScreen(enable: boolean): gdPreviewExportOptions;
|
||||
setIsDevelopmentEnvironment(enable: boolean): gdPreviewExportOptions;
|
||||
setIsInGameEdition(enable: boolean): gdPreviewExportOptions;
|
||||
setNonRuntimeScriptsCacheBurst(value: number): gdPreviewExportOptions;
|
||||
setElectronRemoteRequirePath(electronRemoteRequirePath: string): gdPreviewExportOptions;
|
||||
setGDevelopResourceToken(gdevelopResourceToken: string): gdPreviewExportOptions;
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,654 @@
|
||||
import {
|
||||
AdditiveBlending,
|
||||
Color,
|
||||
DoubleSide,
|
||||
HalfFloatType,
|
||||
Matrix4,
|
||||
MeshDepthMaterial,
|
||||
NoBlending,
|
||||
RGBADepthPacking,
|
||||
ShaderMaterial,
|
||||
UniformsUtils,
|
||||
Vector2,
|
||||
Vector3,
|
||||
WebGLRenderTarget
|
||||
} from 'three';
|
||||
import { Pass, FullScreenQuad } from './Pass.js';
|
||||
import { CopyShader } from '../shaders/CopyShader.js';
|
||||
|
||||
class OutlinePass extends Pass {
|
||||
|
||||
constructor( resolution, scene, camera, selectedObjects ) {
|
||||
|
||||
super();
|
||||
|
||||
this.renderScene = scene;
|
||||
this.renderCamera = camera;
|
||||
this.selectedObjects = selectedObjects !== undefined ? selectedObjects : [];
|
||||
this.visibleEdgeColor = new Color( 1, 1, 1 );
|
||||
this.hiddenEdgeColor = new Color( 0.1, 0.04, 0.02 );
|
||||
this.edgeGlow = 0.0;
|
||||
this.usePatternTexture = false;
|
||||
this.edgeThickness = 1.0;
|
||||
this.edgeStrength = 3.0;
|
||||
this.downSampleRatio = 2;
|
||||
this.pulsePeriod = 0;
|
||||
|
||||
this._visibilityCache = new Map();
|
||||
|
||||
|
||||
this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
|
||||
|
||||
const resx = Math.round( this.resolution.x / this.downSampleRatio );
|
||||
const resy = Math.round( this.resolution.y / this.downSampleRatio );
|
||||
|
||||
this.renderTargetMaskBuffer = new WebGLRenderTarget( this.resolution.x, this.resolution.y );
|
||||
this.renderTargetMaskBuffer.texture.name = 'OutlinePass.mask';
|
||||
this.renderTargetMaskBuffer.texture.generateMipmaps = false;
|
||||
|
||||
this.depthMaterial = new MeshDepthMaterial();
|
||||
this.depthMaterial.side = DoubleSide;
|
||||
this.depthMaterial.depthPacking = RGBADepthPacking;
|
||||
this.depthMaterial.blending = NoBlending;
|
||||
|
||||
this.prepareMaskMaterial = this.getPrepareMaskMaterial();
|
||||
this.prepareMaskMaterial.side = DoubleSide;
|
||||
this.prepareMaskMaterial.fragmentShader = replaceDepthToViewZ( this.prepareMaskMaterial.fragmentShader, this.renderCamera );
|
||||
|
||||
this.renderTargetDepthBuffer = new WebGLRenderTarget( this.resolution.x, this.resolution.y, { type: HalfFloatType } );
|
||||
this.renderTargetDepthBuffer.texture.name = 'OutlinePass.depth';
|
||||
this.renderTargetDepthBuffer.texture.generateMipmaps = false;
|
||||
|
||||
this.renderTargetMaskDownSampleBuffer = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
|
||||
this.renderTargetMaskDownSampleBuffer.texture.name = 'OutlinePass.depthDownSample';
|
||||
this.renderTargetMaskDownSampleBuffer.texture.generateMipmaps = false;
|
||||
|
||||
this.renderTargetBlurBuffer1 = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
|
||||
this.renderTargetBlurBuffer1.texture.name = 'OutlinePass.blur1';
|
||||
this.renderTargetBlurBuffer1.texture.generateMipmaps = false;
|
||||
this.renderTargetBlurBuffer2 = new WebGLRenderTarget( Math.round( resx / 2 ), Math.round( resy / 2 ), { type: HalfFloatType } );
|
||||
this.renderTargetBlurBuffer2.texture.name = 'OutlinePass.blur2';
|
||||
this.renderTargetBlurBuffer2.texture.generateMipmaps = false;
|
||||
|
||||
this.edgeDetectionMaterial = this.getEdgeDetectionMaterial();
|
||||
this.renderTargetEdgeBuffer1 = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
|
||||
this.renderTargetEdgeBuffer1.texture.name = 'OutlinePass.edge1';
|
||||
this.renderTargetEdgeBuffer1.texture.generateMipmaps = false;
|
||||
this.renderTargetEdgeBuffer2 = new WebGLRenderTarget( Math.round( resx / 2 ), Math.round( resy / 2 ), { type: HalfFloatType } );
|
||||
this.renderTargetEdgeBuffer2.texture.name = 'OutlinePass.edge2';
|
||||
this.renderTargetEdgeBuffer2.texture.generateMipmaps = false;
|
||||
|
||||
const MAX_EDGE_THICKNESS = 4;
|
||||
const MAX_EDGE_GLOW = 4;
|
||||
|
||||
this.separableBlurMaterial1 = this.getSeperableBlurMaterial( MAX_EDGE_THICKNESS );
|
||||
this.separableBlurMaterial1.uniforms[ 'texSize' ].value.set( resx, resy );
|
||||
this.separableBlurMaterial1.uniforms[ 'kernelRadius' ].value = 1;
|
||||
this.separableBlurMaterial2 = this.getSeperableBlurMaterial( MAX_EDGE_GLOW );
|
||||
this.separableBlurMaterial2.uniforms[ 'texSize' ].value.set( Math.round( resx / 2 ), Math.round( resy / 2 ) );
|
||||
this.separableBlurMaterial2.uniforms[ 'kernelRadius' ].value = MAX_EDGE_GLOW;
|
||||
|
||||
// Overlay material
|
||||
this.overlayMaterial = this.getOverlayMaterial();
|
||||
|
||||
// copy material
|
||||
|
||||
const copyShader = CopyShader;
|
||||
|
||||
this.copyUniforms = UniformsUtils.clone( copyShader.uniforms );
|
||||
|
||||
this.materialCopy = new ShaderMaterial( {
|
||||
uniforms: this.copyUniforms,
|
||||
vertexShader: copyShader.vertexShader,
|
||||
fragmentShader: copyShader.fragmentShader,
|
||||
blending: NoBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false
|
||||
} );
|
||||
|
||||
this.enabled = true;
|
||||
this.needsSwap = false;
|
||||
|
||||
this._oldClearColor = new Color();
|
||||
this.oldClearAlpha = 1;
|
||||
|
||||
this.fsQuad = new FullScreenQuad( null );
|
||||
|
||||
this.tempPulseColor1 = new Color();
|
||||
this.tempPulseColor2 = new Color();
|
||||
this.textureMatrix = new Matrix4();
|
||||
|
||||
function replaceDepthToViewZ( string, camera ) {
|
||||
|
||||
const type = camera.isPerspectiveCamera ? 'perspective' : 'orthographic';
|
||||
|
||||
return string.replace( /DEPTH_TO_VIEW_Z/g, type + 'DepthToViewZ' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
this.renderTargetMaskBuffer.dispose();
|
||||
this.renderTargetDepthBuffer.dispose();
|
||||
this.renderTargetMaskDownSampleBuffer.dispose();
|
||||
this.renderTargetBlurBuffer1.dispose();
|
||||
this.renderTargetBlurBuffer2.dispose();
|
||||
this.renderTargetEdgeBuffer1.dispose();
|
||||
this.renderTargetEdgeBuffer2.dispose();
|
||||
|
||||
this.depthMaterial.dispose();
|
||||
this.prepareMaskMaterial.dispose();
|
||||
this.edgeDetectionMaterial.dispose();
|
||||
this.separableBlurMaterial1.dispose();
|
||||
this.separableBlurMaterial2.dispose();
|
||||
this.overlayMaterial.dispose();
|
||||
this.materialCopy.dispose();
|
||||
|
||||
this.fsQuad.dispose();
|
||||
|
||||
}
|
||||
|
||||
setSize( width, height ) {
|
||||
|
||||
this.renderTargetMaskBuffer.setSize( width, height );
|
||||
this.renderTargetDepthBuffer.setSize( width, height );
|
||||
|
||||
let resx = Math.round( width / this.downSampleRatio );
|
||||
let resy = Math.round( height / this.downSampleRatio );
|
||||
this.renderTargetMaskDownSampleBuffer.setSize( resx, resy );
|
||||
this.renderTargetBlurBuffer1.setSize( resx, resy );
|
||||
this.renderTargetEdgeBuffer1.setSize( resx, resy );
|
||||
this.separableBlurMaterial1.uniforms[ 'texSize' ].value.set( resx, resy );
|
||||
|
||||
resx = Math.round( resx / 2 );
|
||||
resy = Math.round( resy / 2 );
|
||||
|
||||
this.renderTargetBlurBuffer2.setSize( resx, resy );
|
||||
this.renderTargetEdgeBuffer2.setSize( resx, resy );
|
||||
|
||||
this.separableBlurMaterial2.uniforms[ 'texSize' ].value.set( resx, resy );
|
||||
|
||||
}
|
||||
|
||||
changeVisibilityOfSelectedObjects( bVisible ) {
|
||||
|
||||
const cache = this._visibilityCache;
|
||||
|
||||
function gatherSelectedMeshesCallBack( object ) {
|
||||
|
||||
if ( object.isMesh ) {
|
||||
|
||||
if ( bVisible === true ) {
|
||||
|
||||
object.visible = cache.get( object );
|
||||
|
||||
} else {
|
||||
|
||||
cache.set( object, object.visible );
|
||||
object.visible = bVisible;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
|
||||
|
||||
const selectedObject = this.selectedObjects[ i ];
|
||||
selectedObject.traverse( gatherSelectedMeshesCallBack );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
changeVisibilityOfNonSelectedObjects( bVisible ) {
|
||||
|
||||
const cache = this._visibilityCache;
|
||||
const selectedMeshes = [];
|
||||
|
||||
function gatherSelectedMeshesCallBack( object ) {
|
||||
|
||||
if ( object.isMesh ) selectedMeshes.push( object );
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < this.selectedObjects.length; i ++ ) {
|
||||
|
||||
const selectedObject = this.selectedObjects[ i ];
|
||||
selectedObject.traverse( gatherSelectedMeshesCallBack );
|
||||
|
||||
}
|
||||
|
||||
function VisibilityChangeCallBack( object ) {
|
||||
|
||||
if ( object.isMesh || object.isSprite ) {
|
||||
|
||||
// only meshes and sprites are supported by OutlinePass
|
||||
|
||||
let bFound = false;
|
||||
|
||||
for ( let i = 0; i < selectedMeshes.length; i ++ ) {
|
||||
|
||||
const selectedObjectId = selectedMeshes[ i ].id;
|
||||
|
||||
if ( selectedObjectId === object.id ) {
|
||||
|
||||
bFound = true;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( bFound === false ) {
|
||||
|
||||
const visibility = object.visible;
|
||||
|
||||
if ( bVisible === false || cache.get( object ) === true ) {
|
||||
|
||||
object.visible = bVisible;
|
||||
|
||||
}
|
||||
|
||||
cache.set( object, visibility );
|
||||
|
||||
}
|
||||
|
||||
} else if ( object.isPoints || object.isLine ) {
|
||||
|
||||
// the visibilty of points and lines is always set to false in order to
|
||||
// not affect the outline computation
|
||||
|
||||
if ( bVisible === true ) {
|
||||
|
||||
object.visible = cache.get( object ); // restore
|
||||
|
||||
} else {
|
||||
|
||||
cache.set( object, object.visible );
|
||||
object.visible = bVisible;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.renderScene.traverse( VisibilityChangeCallBack );
|
||||
|
||||
}
|
||||
|
||||
updateTextureMatrix() {
|
||||
|
||||
this.textureMatrix.set( 0.5, 0.0, 0.0, 0.5,
|
||||
0.0, 0.5, 0.0, 0.5,
|
||||
0.0, 0.0, 0.5, 0.5,
|
||||
0.0, 0.0, 0.0, 1.0 );
|
||||
this.textureMatrix.multiply( this.renderCamera.projectionMatrix );
|
||||
this.textureMatrix.multiply( this.renderCamera.matrixWorldInverse );
|
||||
|
||||
}
|
||||
|
||||
render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
|
||||
|
||||
if ( this.selectedObjects.length > 0 ) {
|
||||
|
||||
renderer.getClearColor( this._oldClearColor );
|
||||
this.oldClearAlpha = renderer.getClearAlpha();
|
||||
const oldAutoClear = renderer.autoClear;
|
||||
|
||||
renderer.autoClear = false;
|
||||
|
||||
if ( maskActive ) renderer.state.buffers.stencil.setTest( false );
|
||||
|
||||
renderer.setClearColor( 0xffffff, 1 );
|
||||
|
||||
// Make selected objects invisible
|
||||
this.changeVisibilityOfSelectedObjects( false );
|
||||
|
||||
const currentBackground = this.renderScene.background;
|
||||
this.renderScene.background = null;
|
||||
|
||||
// 1. Draw Non Selected objects in the depth buffer
|
||||
this.renderScene.overrideMaterial = this.depthMaterial;
|
||||
renderer.setRenderTarget( this.renderTargetDepthBuffer );
|
||||
renderer.clear();
|
||||
renderer.render( this.renderScene, this.renderCamera );
|
||||
|
||||
// Make selected objects visible
|
||||
this.changeVisibilityOfSelectedObjects( true );
|
||||
this._visibilityCache.clear();
|
||||
|
||||
// Update Texture Matrix for Depth compare
|
||||
this.updateTextureMatrix();
|
||||
|
||||
// Make non selected objects invisible, and draw only the selected objects, by comparing the depth buffer of non selected objects
|
||||
this.changeVisibilityOfNonSelectedObjects( false );
|
||||
this.renderScene.overrideMaterial = this.prepareMaskMaterial;
|
||||
this.prepareMaskMaterial.uniforms[ 'cameraNearFar' ].value.set( this.renderCamera.near, this.renderCamera.far );
|
||||
this.prepareMaskMaterial.uniforms[ 'depthTexture' ].value = this.renderTargetDepthBuffer.texture;
|
||||
this.prepareMaskMaterial.uniforms[ 'textureMatrix' ].value = this.textureMatrix;
|
||||
renderer.setRenderTarget( this.renderTargetMaskBuffer );
|
||||
renderer.clear();
|
||||
renderer.render( this.renderScene, this.renderCamera );
|
||||
this.renderScene.overrideMaterial = null;
|
||||
this.changeVisibilityOfNonSelectedObjects( true );
|
||||
this._visibilityCache.clear();
|
||||
|
||||
this.renderScene.background = currentBackground;
|
||||
|
||||
// 2. Downsample to Half resolution
|
||||
this.fsQuad.material = this.materialCopy;
|
||||
this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetMaskBuffer.texture;
|
||||
renderer.setRenderTarget( this.renderTargetMaskDownSampleBuffer );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
this.tempPulseColor1.copy( this.visibleEdgeColor );
|
||||
this.tempPulseColor2.copy( this.hiddenEdgeColor );
|
||||
|
||||
if ( this.pulsePeriod > 0 ) {
|
||||
|
||||
const scalar = ( 1 + 0.25 ) / 2 + Math.cos( performance.now() * 0.01 / this.pulsePeriod ) * ( 1.0 - 0.25 ) / 2;
|
||||
this.tempPulseColor1.multiplyScalar( scalar );
|
||||
this.tempPulseColor2.multiplyScalar( scalar );
|
||||
|
||||
}
|
||||
|
||||
// 3. Apply Edge Detection Pass
|
||||
this.fsQuad.material = this.edgeDetectionMaterial;
|
||||
this.edgeDetectionMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskDownSampleBuffer.texture;
|
||||
this.edgeDetectionMaterial.uniforms[ 'texSize' ].value.set( this.renderTargetMaskDownSampleBuffer.width, this.renderTargetMaskDownSampleBuffer.height );
|
||||
this.edgeDetectionMaterial.uniforms[ 'visibleEdgeColor' ].value = this.tempPulseColor1;
|
||||
this.edgeDetectionMaterial.uniforms[ 'hiddenEdgeColor' ].value = this.tempPulseColor2;
|
||||
renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
// 4. Apply Blur on Half res
|
||||
this.fsQuad.material = this.separableBlurMaterial1;
|
||||
this.separableBlurMaterial1.uniforms[ 'colorTexture' ].value = this.renderTargetEdgeBuffer1.texture;
|
||||
this.separableBlurMaterial1.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionX;
|
||||
this.separableBlurMaterial1.uniforms[ 'kernelRadius' ].value = this.edgeThickness;
|
||||
renderer.setRenderTarget( this.renderTargetBlurBuffer1 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
this.separableBlurMaterial1.uniforms[ 'colorTexture' ].value = this.renderTargetBlurBuffer1.texture;
|
||||
this.separableBlurMaterial1.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionY;
|
||||
renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
// Apply Blur on quarter res
|
||||
this.fsQuad.material = this.separableBlurMaterial2;
|
||||
this.separableBlurMaterial2.uniforms[ 'colorTexture' ].value = this.renderTargetEdgeBuffer1.texture;
|
||||
this.separableBlurMaterial2.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionX;
|
||||
renderer.setRenderTarget( this.renderTargetBlurBuffer2 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
this.separableBlurMaterial2.uniforms[ 'colorTexture' ].value = this.renderTargetBlurBuffer2.texture;
|
||||
this.separableBlurMaterial2.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionY;
|
||||
renderer.setRenderTarget( this.renderTargetEdgeBuffer2 );
|
||||
renderer.clear();
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
// Blend it additively over the input texture
|
||||
this.fsQuad.material = this.overlayMaterial;
|
||||
this.overlayMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskBuffer.texture;
|
||||
this.overlayMaterial.uniforms[ 'edgeTexture1' ].value = this.renderTargetEdgeBuffer1.texture;
|
||||
this.overlayMaterial.uniforms[ 'edgeTexture2' ].value = this.renderTargetEdgeBuffer2.texture;
|
||||
this.overlayMaterial.uniforms[ 'patternTexture' ].value = this.patternTexture;
|
||||
this.overlayMaterial.uniforms[ 'edgeStrength' ].value = this.edgeStrength;
|
||||
this.overlayMaterial.uniforms[ 'edgeGlow' ].value = this.edgeGlow;
|
||||
this.overlayMaterial.uniforms[ 'usePatternTexture' ].value = this.usePatternTexture;
|
||||
|
||||
|
||||
if ( maskActive ) renderer.state.buffers.stencil.setTest( true );
|
||||
|
||||
renderer.setRenderTarget( readBuffer );
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
renderer.setClearColor( this._oldClearColor, this.oldClearAlpha );
|
||||
renderer.autoClear = oldAutoClear;
|
||||
|
||||
}
|
||||
|
||||
if ( this.renderToScreen ) {
|
||||
|
||||
this.fsQuad.material = this.materialCopy;
|
||||
this.copyUniforms[ 'tDiffuse' ].value = readBuffer.texture;
|
||||
renderer.setRenderTarget( null );
|
||||
this.fsQuad.render( renderer );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getPrepareMaskMaterial() {
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
uniforms: {
|
||||
'depthTexture': { value: null },
|
||||
'cameraNearFar': { value: new Vector2( 0.5, 0.5 ) },
|
||||
'textureMatrix': { value: null }
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`#include <morphtarget_pars_vertex>
|
||||
#include <skinning_pars_vertex>
|
||||
|
||||
varying vec4 projTexCoord;
|
||||
varying vec4 vPosition;
|
||||
uniform mat4 textureMatrix;
|
||||
|
||||
void main() {
|
||||
|
||||
#include <skinbase_vertex>
|
||||
#include <begin_vertex>
|
||||
#include <morphtarget_vertex>
|
||||
#include <skinning_vertex>
|
||||
#include <project_vertex>
|
||||
|
||||
vPosition = mvPosition;
|
||||
|
||||
vec4 worldPosition = vec4( transformed, 1.0 );
|
||||
|
||||
#ifdef USE_INSTANCING
|
||||
|
||||
worldPosition = instanceMatrix * worldPosition;
|
||||
|
||||
#endif
|
||||
|
||||
worldPosition = modelMatrix * worldPosition;
|
||||
|
||||
projTexCoord = textureMatrix * worldPosition;
|
||||
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`#include <packing>
|
||||
varying vec4 vPosition;
|
||||
varying vec4 projTexCoord;
|
||||
uniform sampler2D depthTexture;
|
||||
uniform vec2 cameraNearFar;
|
||||
|
||||
void main() {
|
||||
|
||||
float depth = unpackRGBAToDepth(texture2DProj( depthTexture, projTexCoord ));
|
||||
float viewZ = - DEPTH_TO_VIEW_Z( depth, cameraNearFar.x, cameraNearFar.y );
|
||||
float depthTest = (-vPosition.z > viewZ) ? 1.0 : 0.0;
|
||||
gl_FragColor = vec4(0.0, depthTest, 1.0, 1.0);
|
||||
|
||||
}`
|
||||
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
getEdgeDetectionMaterial() {
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
uniforms: {
|
||||
'maskTexture': { value: null },
|
||||
'texSize': { value: new Vector2( 0.5, 0.5 ) },
|
||||
'visibleEdgeColor': { value: new Vector3( 1.0, 1.0, 1.0 ) },
|
||||
'hiddenEdgeColor': { value: new Vector3( 1.0, 1.0, 1.0 ) },
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
uniform sampler2D maskTexture;
|
||||
uniform vec2 texSize;
|
||||
uniform vec3 visibleEdgeColor;
|
||||
uniform vec3 hiddenEdgeColor;
|
||||
|
||||
void main() {
|
||||
vec2 invSize = 1.0 / texSize;
|
||||
vec4 uvOffset = vec4(1.0, 0.0, 0.0, 1.0) * vec4(invSize, invSize);
|
||||
vec4 c1 = texture2D( maskTexture, vUv + uvOffset.xy);
|
||||
vec4 c2 = texture2D( maskTexture, vUv - uvOffset.xy);
|
||||
vec4 c3 = texture2D( maskTexture, vUv + uvOffset.yw);
|
||||
vec4 c4 = texture2D( maskTexture, vUv - uvOffset.yw);
|
||||
float diff1 = (c1.r - c2.r)*0.5;
|
||||
float diff2 = (c3.r - c4.r)*0.5;
|
||||
float d = length( vec2(diff1, diff2) );
|
||||
float a1 = min(c1.g, c2.g);
|
||||
float a2 = min(c3.g, c4.g);
|
||||
float visibilityFactor = min(a1, a2);
|
||||
vec3 edgeColor = 1.0 - visibilityFactor > 0.001 ? visibleEdgeColor : hiddenEdgeColor;
|
||||
gl_FragColor = vec4(edgeColor, 1.0) * vec4(d);
|
||||
}`
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
getSeperableBlurMaterial( maxRadius ) {
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
defines: {
|
||||
'MAX_RADIUS': maxRadius,
|
||||
},
|
||||
|
||||
uniforms: {
|
||||
'colorTexture': { value: null },
|
||||
'texSize': { value: new Vector2( 0.5, 0.5 ) },
|
||||
'direction': { value: new Vector2( 0.5, 0.5 ) },
|
||||
'kernelRadius': { value: 1.0 }
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`#include <common>
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D colorTexture;
|
||||
uniform vec2 texSize;
|
||||
uniform vec2 direction;
|
||||
uniform float kernelRadius;
|
||||
|
||||
float gaussianPdf(in float x, in float sigma) {
|
||||
return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 invSize = 1.0 / texSize;
|
||||
float sigma = kernelRadius/2.0;
|
||||
float weightSum = gaussianPdf(0.0, sigma);
|
||||
vec4 diffuseSum = texture2D( colorTexture, vUv) * weightSum;
|
||||
vec2 delta = direction * invSize * kernelRadius/float(MAX_RADIUS);
|
||||
vec2 uvOffset = delta;
|
||||
for( int i = 1; i <= MAX_RADIUS; i ++ ) {
|
||||
float x = kernelRadius * float(i) / float(MAX_RADIUS);
|
||||
float w = gaussianPdf(x, sigma);
|
||||
vec4 sample1 = texture2D( colorTexture, vUv + uvOffset);
|
||||
vec4 sample2 = texture2D( colorTexture, vUv - uvOffset);
|
||||
diffuseSum += ((sample1 + sample2) * w);
|
||||
weightSum += (2.0 * w);
|
||||
uvOffset += delta;
|
||||
}
|
||||
gl_FragColor = diffuseSum/weightSum;
|
||||
}`
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
getOverlayMaterial() {
|
||||
|
||||
return new ShaderMaterial( {
|
||||
|
||||
uniforms: {
|
||||
'maskTexture': { value: null },
|
||||
'edgeTexture1': { value: null },
|
||||
'edgeTexture2': { value: null },
|
||||
'patternTexture': { value: null },
|
||||
'edgeStrength': { value: 1.0 },
|
||||
'edgeGlow': { value: 1.0 },
|
||||
'usePatternTexture': { value: 0.0 }
|
||||
},
|
||||
|
||||
vertexShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
||||
}`,
|
||||
|
||||
fragmentShader:
|
||||
`varying vec2 vUv;
|
||||
|
||||
uniform sampler2D maskTexture;
|
||||
uniform sampler2D edgeTexture1;
|
||||
uniform sampler2D edgeTexture2;
|
||||
uniform sampler2D patternTexture;
|
||||
uniform float edgeStrength;
|
||||
uniform float edgeGlow;
|
||||
uniform bool usePatternTexture;
|
||||
|
||||
void main() {
|
||||
vec4 edgeValue1 = texture2D(edgeTexture1, vUv);
|
||||
vec4 edgeValue2 = texture2D(edgeTexture2, vUv);
|
||||
vec4 maskColor = texture2D(maskTexture, vUv);
|
||||
vec4 patternColor = texture2D(patternTexture, 6.0 * vUv);
|
||||
float visibilityFactor = 1.0 - maskColor.g > 0.0 ? 1.0 : 0.5;
|
||||
vec4 edgeValue = edgeValue1 + edgeValue2 * edgeGlow;
|
||||
vec4 finalColor = edgeStrength * maskColor.r * edgeValue;
|
||||
if(usePatternTexture)
|
||||
finalColor += + visibilityFactor * (1.0 - maskColor.r) * (1.0 - patternColor.r);
|
||||
gl_FragColor = finalColor;
|
||||
}`,
|
||||
blending: AdditiveBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
transparent: true
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OutlinePass.BlurDirectionX = new Vector2( 1.0, 0.0 );
|
||||
OutlinePass.BlurDirectionY = new Vector2( 0.0, 1.0 );
|
||||
|
||||
export { OutlinePass };
|
@@ -5,14 +5,19 @@
|
||||
|
||||
export { GLTFLoader } from "./examples/jsm/loaders/GLTFLoader";
|
||||
export { DRACOLoader } from "./examples/jsm/loaders/DRACOLoader";
|
||||
|
||||
export * as SkeletonUtils from "./examples/jsm/utils/SkeletonUtils";
|
||||
|
||||
export { TransformControls } from "./examples/jsm/controls/TransformControls";
|
||||
|
||||
export { EffectComposer } from "./examples/jsm/postprocessing/EffectComposer";
|
||||
export { RenderPass } from "./examples/jsm/postprocessing/RenderPass";
|
||||
export { OutlinePass } from "./examples/jsm/postprocessing/OutlinePass";
|
||||
export { ShaderPass } from "./examples/jsm/postprocessing/ShaderPass";
|
||||
export { SMAAPass } from "./examples/jsm/postprocessing/SMAAPass";
|
||||
export { OutputPass } from "./examples/jsm/postprocessing/OutputPass";
|
||||
export { UnrealBloomPass } from "./examples/jsm/postprocessing/UnrealBloomPass";
|
||||
|
||||
export { BrightnessContrastShader } from "./examples/jsm/shaders/BrightnessContrastShader";
|
||||
export { HueSaturationShader } from "./examples/jsm/shaders/HueSaturationShader";
|
||||
export { ExposureShader } from "./examples/jsm/shaders/ExposureShader";
|
||||
|
@@ -51,18 +51,19 @@ export default class CommandManager implements CommandManagerInterface {
|
||||
}
|
||||
|
||||
registerCommand = (commandName: CommandName, command: Command) => {
|
||||
if (this._commands[commandName])
|
||||
return console.warn(
|
||||
`Tried to register command ${commandName}, but it is already registered.`
|
||||
);
|
||||
if (this._commands[commandName]) return;
|
||||
// if (this._commands[commandName])
|
||||
// return console.warn(
|
||||
// `Tried to register command ${commandName}, but it is already registered.`
|
||||
// );
|
||||
this._commands[commandName] = command;
|
||||
};
|
||||
|
||||
deregisterCommand = (commandName: CommandName) => {
|
||||
if (!this._commands[commandName])
|
||||
return console.warn(
|
||||
`Tried to deregister command ${commandName}, but it is not registered.`
|
||||
);
|
||||
// if (!this._commands[commandName])
|
||||
// return console.warn(
|
||||
// `Tried to deregister command ${commandName}, but it is not registered.`
|
||||
// );
|
||||
delete this._commands[commandName];
|
||||
};
|
||||
|
||||
|
@@ -1,38 +1,62 @@
|
||||
// @flow
|
||||
import { t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import { type DebuggerId } from '../ExportAndShare/PreviewLauncher.flow';
|
||||
import {
|
||||
type DebuggerId,
|
||||
type DebuggerStatus,
|
||||
} from '../ExportAndShare/PreviewLauncher.flow';
|
||||
|
||||
type Props = {|
|
||||
selectedId: DebuggerId,
|
||||
debuggerIds: Array<DebuggerId>,
|
||||
debuggerStatus: { [DebuggerId]: DebuggerStatus },
|
||||
onChooseDebugger: DebuggerId => void,
|
||||
|};
|
||||
|
||||
export default class DebuggerSelector extends React.Component<Props, void> {
|
||||
render() {
|
||||
const hasDebuggers = !!this.props.debuggerIds.length;
|
||||
const debuggerIds = Object.keys(this.props.debuggerStatus);
|
||||
const hasDebuggers = !!debuggerIds.length;
|
||||
return (
|
||||
<SelectField
|
||||
fullWidth
|
||||
value={hasDebuggers ? this.props.selectedId : 0}
|
||||
onChange={(e, i, value) =>
|
||||
this.props.onChooseDebugger(parseInt(value, 10) || 0)
|
||||
}
|
||||
disabled={!hasDebuggers}
|
||||
>
|
||||
{this.props.debuggerIds.map(id => (
|
||||
<SelectOption value={id} key={id} label={t`Game preview #${id}`} />
|
||||
))}
|
||||
{!hasDebuggers && (
|
||||
<SelectOption
|
||||
value={0}
|
||||
label={t`No preview running. Run a preview and you will be able to inspect it with the debugger`}
|
||||
/>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<SelectField
|
||||
fullWidth
|
||||
value={hasDebuggers ? this.props.selectedId : 0}
|
||||
onChange={(e, i, value) =>
|
||||
this.props.onChooseDebugger(parseInt(value, 10) || 0)
|
||||
}
|
||||
disabled={!hasDebuggers}
|
||||
>
|
||||
{debuggerIds.map(id => {
|
||||
const status = this.props.debuggerStatus[+id];
|
||||
const statusText = status.isPaused
|
||||
? status.isInGameEdition
|
||||
? t`Editing`
|
||||
: t`Paused`
|
||||
: status.isInGameEdition
|
||||
? t`Playing (in-game edition)`
|
||||
: t`Playing`;
|
||||
|
||||
return (
|
||||
<SelectOption
|
||||
value={+id}
|
||||
key={id}
|
||||
label={t`Game preview #${id} (${i18n._(statusText)})`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!hasDebuggers && (
|
||||
<SelectOption
|
||||
value={0}
|
||||
label={t`No preview running. Run a preview and you will be able to inspect it with the debugger`}
|
||||
/>
|
||||
)}
|
||||
</SelectField>
|
||||
)}
|
||||
</SelectField>
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import EmptyMessage from '../UI/EmptyMessage';
|
||||
import {
|
||||
type PreviewDebuggerServer,
|
||||
type DebuggerId,
|
||||
type DebuggerStatus,
|
||||
} from '../ExportAndShare/PreviewLauncher.flow';
|
||||
import { type Log, LogsManager } from './DebuggerConsole';
|
||||
|
||||
@@ -53,7 +54,7 @@ type State = {|
|
||||
debuggerGameData: { [DebuggerId]: any },
|
||||
profilerOutputs: { [DebuggerId]: ProfilerOutput },
|
||||
profilingInProgress: { [DebuggerId]: boolean },
|
||||
gameIsPaused: { [DebuggerId]: boolean },
|
||||
debuggerStatus: { [DebuggerId]: DebuggerStatus },
|
||||
selectedId: DebuggerId,
|
||||
logs: { [DebuggerId]: Array<Log> },
|
||||
|};
|
||||
@@ -70,7 +71,7 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
debuggerGameData: {},
|
||||
profilerOutputs: {},
|
||||
profilingInProgress: {},
|
||||
gameIsPaused: {},
|
||||
debuggerStatus: {},
|
||||
selectedId: 0,
|
||||
logs: {},
|
||||
};
|
||||
@@ -79,18 +80,22 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
_debuggerLogs: Map<number, LogsManager> = new Map();
|
||||
|
||||
updateToolbar = () => {
|
||||
const { selectedId, gameIsPaused } = this.state;
|
||||
const { selectedId, debuggerStatus } = this.state;
|
||||
|
||||
const selectedDebuggerContents = this._debuggerContents[
|
||||
this.state.selectedId
|
||||
];
|
||||
|
||||
const isSelectedDebuggerPaused = debuggerStatus[selectedId]
|
||||
? debuggerStatus[selectedId].isPaused
|
||||
: false;
|
||||
|
||||
this.props.setToolbar(
|
||||
<Toolbar
|
||||
onPlay={() => this._play(this.state.selectedId)}
|
||||
onPause={() => this._pause(this.state.selectedId)}
|
||||
canPlay={this._hasSelectedDebugger() && gameIsPaused[selectedId]}
|
||||
canPause={this._hasSelectedDebugger() && !gameIsPaused[selectedId]}
|
||||
canPlay={this._hasSelectedDebugger() && isSelectedDebuggerPaused}
|
||||
canPause={this._hasSelectedDebugger() && !isSelectedDebuggerPaused}
|
||||
canOpenProfiler={this._hasSelectedDebugger()}
|
||||
isProfilerShown={
|
||||
!!selectedDebuggerContents &&
|
||||
@@ -161,14 +166,14 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
debuggerGameData,
|
||||
profilerOutputs,
|
||||
profilingInProgress,
|
||||
gameIsPaused,
|
||||
debuggerStatus,
|
||||
}) => {
|
||||
// Remove any data bound to the instance that might have been stored.
|
||||
// Otherwise this would be a memory leak.
|
||||
if (debuggerGameData[id]) delete debuggerGameData[id];
|
||||
if (profilerOutputs[id]) delete profilerOutputs[id];
|
||||
if (profilingInProgress[id]) delete profilingInProgress[id];
|
||||
if (gameIsPaused[id]) delete gameIsPaused[id];
|
||||
if (debuggerStatus[id]) delete debuggerStatus[id];
|
||||
|
||||
return {
|
||||
debuggerIds,
|
||||
@@ -181,7 +186,7 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
debuggerGameData,
|
||||
profilerOutputs,
|
||||
profilingInProgress,
|
||||
gameIsPaused,
|
||||
debuggerStatus,
|
||||
};
|
||||
},
|
||||
() => this.updateToolbar()
|
||||
@@ -219,6 +224,11 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
this.setState({
|
||||
unregisterDebuggerServerCallbacks: unregisterCallbacks,
|
||||
});
|
||||
|
||||
// Fetch the status of each debugger client.
|
||||
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
|
||||
previewDebuggerServer.sendMessage(debuggerId, { command: 'getStatus' });
|
||||
});
|
||||
};
|
||||
|
||||
_handleMessage = (id: DebuggerId, data: any) => {
|
||||
@@ -229,6 +239,16 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
[id]: data.payload,
|
||||
},
|
||||
});
|
||||
} else if (data.command === 'status') {
|
||||
this.setState(
|
||||
state => ({
|
||||
debuggerStatus: {
|
||||
...state.debuggerStatus,
|
||||
[id]: data.payload,
|
||||
},
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
} else if (data.command === 'profiler.output') {
|
||||
this.setState({
|
||||
profilerOutputs: {
|
||||
@@ -244,22 +264,10 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
this.setState(state => ({
|
||||
profilingInProgress: { ...state.profilingInProgress, [id]: false },
|
||||
}));
|
||||
} else if (data.command === 'game.resumed') {
|
||||
this.setState(
|
||||
state => ({
|
||||
gameIsPaused: { ...state.gameIsPaused, [id]: false },
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
} else if (data.command === 'game.paused') {
|
||||
this.setState(
|
||||
state => ({
|
||||
gameIsPaused: { ...state.gameIsPaused, [id]: true },
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
} else if (data.command === 'hotReloader.logs') {
|
||||
// Nothing to do.
|
||||
} else if (data.command === 'instances.updated') {
|
||||
// Nothing to do.
|
||||
} else if (data.command === 'console.log') {
|
||||
// Filter out unavoidable warnings that do not concern non-engine devs.
|
||||
if (isUnavoidableLibraryWarning(data.payload)) return;
|
||||
@@ -276,24 +284,14 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
const { previewDebuggerServer } = this.props;
|
||||
previewDebuggerServer.sendMessage(id, { command: 'play' });
|
||||
|
||||
this.setState(
|
||||
state => ({
|
||||
gameIsPaused: { ...state.gameIsPaused, [id]: false },
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
// Pause status is transmitted by the game (using `status`).
|
||||
};
|
||||
|
||||
_pause = (id: DebuggerId) => {
|
||||
const { previewDebuggerServer } = this.props;
|
||||
previewDebuggerServer.sendMessage(id, { command: 'pause' });
|
||||
|
||||
this.setState(
|
||||
state => ({
|
||||
gameIsPaused: { ...state.gameIsPaused, [id]: true },
|
||||
}),
|
||||
() => this.updateToolbar()
|
||||
);
|
||||
// Pause status is transmitted by the game (using `status`).
|
||||
};
|
||||
|
||||
_refresh = (id: DebuggerId) => {
|
||||
@@ -345,7 +343,7 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
debuggerServerError,
|
||||
debuggerServerState,
|
||||
selectedId,
|
||||
debuggerIds,
|
||||
debuggerStatus,
|
||||
debuggerGameData,
|
||||
profilerOutputs,
|
||||
profilingInProgress,
|
||||
@@ -375,7 +373,7 @@ export default class Debugger extends React.Component<Props, State> {
|
||||
<Column expand noMargin>
|
||||
<DebuggerSelector
|
||||
selectedId={selectedId}
|
||||
debuggerIds={debuggerIds}
|
||||
debuggerStatus={debuggerStatus}
|
||||
onChooseDebugger={id =>
|
||||
this.setState(
|
||||
{
|
||||
|
115
newIDE/app/src/EmbeddedGame/EmbeddedGameFrame.js
Normal file
115
newIDE/app/src/EmbeddedGame/EmbeddedGameFrame.js
Normal file
@@ -0,0 +1,115 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { type PreviewDebuggerServer } from '../ExportAndShare/PreviewLauncher.flow';
|
||||
|
||||
type AttachToPreviewOptions = {|
|
||||
previewIndexHtmlLocation: string,
|
||||
|};
|
||||
|
||||
type SwitchToSceneEditionOptions = {|
|
||||
sceneName: string,
|
||||
externalLayoutName?: string,
|
||||
|};
|
||||
|
||||
let onAttachToPreview: null | (AttachToPreviewOptions => void) = null;
|
||||
let onSwitchToSceneEdition: null | (SwitchToSceneEditionOptions => void) = null;
|
||||
|
||||
export const attachToPreview = ({
|
||||
previewIndexHtmlLocation,
|
||||
}: AttachToPreviewOptions) => {
|
||||
if (!onAttachToPreview) throw new Error('No EmbeddedGameFrame registered.');
|
||||
onAttachToPreview({ previewIndexHtmlLocation });
|
||||
};
|
||||
|
||||
export const switchToSceneEdition = ({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
}: SwitchToSceneEditionOptions) => {
|
||||
if (!onSwitchToSceneEdition)
|
||||
throw new Error('No EmbeddedGameFrame registered.');
|
||||
onSwitchToSceneEdition({ sceneName, externalLayoutName });
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
previewDebuggerServer: PreviewDebuggerServer | null,
|
||||
onLaunchPreviewForInGameEdition: ({|
|
||||
sceneName: string,
|
||||
externalLayoutName: ?string,
|
||||
|}) => void,
|
||||
|};
|
||||
|
||||
export const EmbeddedGameFrame = ({
|
||||
previewDebuggerServer,
|
||||
onLaunchPreviewForInGameEdition,
|
||||
}: Props) => {
|
||||
const [
|
||||
previewIndexHtmlLocation,
|
||||
setPreviewIndexHtmlLocation,
|
||||
] = React.useState<string>('');
|
||||
const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
// TODO: use a real context for this?
|
||||
onAttachToPreview = (options: AttachToPreviewOptions) => {
|
||||
setPreviewIndexHtmlLocation(options.previewIndexHtmlLocation);
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.contentWindow.focus();
|
||||
}
|
||||
};
|
||||
onSwitchToSceneEdition = (options: SwitchToSceneEditionOptions) => {
|
||||
if (!previewDebuggerServer) return;
|
||||
|
||||
const { sceneName, externalLayoutName } = options;
|
||||
|
||||
if (!previewIndexHtmlLocation) {
|
||||
console.info(
|
||||
externalLayoutName
|
||||
? `Launching in-game edition preview for external layout "${externalLayoutName}" (scene: "${sceneName}").`
|
||||
: `Launching in-game edition preview for scene "${sceneName}".`
|
||||
);
|
||||
onLaunchPreviewForInGameEdition({ sceneName, externalLayoutName });
|
||||
} else {
|
||||
console.info(
|
||||
externalLayoutName
|
||||
? `Switching in-game edition previews to external layout "${externalLayoutName}" (scene: "${sceneName}").`
|
||||
: `Switching in-game edition previews to scene "${sceneName}".`
|
||||
);
|
||||
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
|
||||
previewDebuggerServer.sendMessage(debuggerId, {
|
||||
command: 'switchForInGameEdition',
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
previewDebuggerServer,
|
||||
previewIndexHtmlLocation,
|
||||
onLaunchPreviewForInGameEdition,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Game Preview"
|
||||
src={previewIndexHtmlLocation}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -20,6 +20,7 @@ import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternal
|
||||
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
||||
import { isNativeMobileApp } from '../../../Utils/Platform';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
import { attachToPreview } from '../../../EmbeddedGame/EmbeddedGameFrame';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type State = {|
|
||||
@@ -158,6 +159,9 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
);
|
||||
previewExportOptions.setLayoutName(layout.getName());
|
||||
previewExportOptions.setIsDevelopmentEnvironment(Window.isDev());
|
||||
previewExportOptions.setIsInGameEdition(
|
||||
previewOptions.isForInGameEdition
|
||||
);
|
||||
if (externalLayout) {
|
||||
previewExportOptions.setExternalLayoutName(externalLayout.getName());
|
||||
}
|
||||
@@ -220,6 +224,12 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
// Upload any file that must be exported for the preview.
|
||||
await browserS3FileSystem.uploadPendingObjects();
|
||||
|
||||
if (previewOptions.isForInGameEdition) {
|
||||
attachToPreview({
|
||||
previewIndexHtmlLocation: outputDir + '/index.html',
|
||||
});
|
||||
}
|
||||
|
||||
// Change the HTML file displayed by the preview window so that it starts loading
|
||||
// the game.
|
||||
previewWindows.forEach(
|
||||
|
@@ -4,7 +4,6 @@ import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import LocalFileSystem from '../LocalFileSystem';
|
||||
import optionalRequire from '../../../Utils/OptionalRequire';
|
||||
import { timeFunction } from '../../../Utils/TimeFunction';
|
||||
import { findGDJS } from '../../../GameEngineFinder/LocalGDJSFinder';
|
||||
import LocalNetworkPreviewDialog from './LocalNetworkPreviewDialog';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
} from './LocalPreviewDebuggerServer';
|
||||
import Window from '../../../Utils/Window';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
import { attachToPreview } from '../../../EmbeddedGame/EmbeddedGameFrame';
|
||||
const electron = optionalRequire('electron');
|
||||
const path = optionalRequire('path');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
@@ -47,6 +47,28 @@ type State = {|
|
||||
captureOptions: ?CaptureOptions,
|
||||
|};
|
||||
|
||||
const prepareExporter = async (): Promise<{|
|
||||
outputDir: string,
|
||||
exporter: gdjsExporter,
|
||||
gdjsRoot: string,
|
||||
|}> => {
|
||||
const { gdjsRoot } = await findGDJS();
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const localFileSystem = new LocalFileSystem({
|
||||
downloadUrlsToLocalFiles: false,
|
||||
});
|
||||
const fileSystem = assignIn(new gd.AbstractFileSystemJS(), localFileSystem);
|
||||
const outputDir = path.join(fileSystem.getTempDir(), 'preview');
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
outputDir,
|
||||
exporter,
|
||||
gdjsRoot,
|
||||
};
|
||||
};
|
||||
|
||||
export default class LocalPreviewLauncher extends React.Component<
|
||||
PreviewLauncherProps,
|
||||
State
|
||||
@@ -171,168 +193,138 @@ export default class LocalPreviewLauncher extends React.Component<
|
||||
);
|
||||
};
|
||||
|
||||
_prepareExporter = (): Promise<{|
|
||||
outputDir: string,
|
||||
exporter: gdjsExporter,
|
||||
gdjsRoot: string,
|
||||
|}> => {
|
||||
return findGDJS().then(({ gdjsRoot }) => {
|
||||
console.info('GDJS found in ', gdjsRoot);
|
||||
|
||||
const localFileSystem = new LocalFileSystem({
|
||||
downloadUrlsToLocalFiles: false,
|
||||
});
|
||||
const fileSystem = assignIn(
|
||||
new gd.AbstractFileSystemJS(),
|
||||
localFileSystem
|
||||
);
|
||||
const outputDir = path.join(fileSystem.getTempDir(), 'preview');
|
||||
const exporter = new gd.Exporter(fileSystem, gdjsRoot);
|
||||
|
||||
return {
|
||||
outputDir,
|
||||
exporter,
|
||||
gdjsRoot,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
launchPreview = (previewOptions: PreviewOptions): Promise<any> => {
|
||||
launchPreview = async (previewOptions: PreviewOptions): Promise<any> => {
|
||||
const { project, layout, externalLayout } = previewOptions;
|
||||
|
||||
// Start the debugger server for previews. Even if not used,
|
||||
// useful if the user opens the Debugger editor later, or want to
|
||||
// hot reload.
|
||||
return this.getPreviewDebuggerServer()
|
||||
.startServer()
|
||||
.catch(err => {
|
||||
// Ignore any error when running the debugger server - the preview
|
||||
// can still work without it.
|
||||
console.error(
|
||||
'Unable to start the Debugger Server for the preview:',
|
||||
err
|
||||
);
|
||||
})
|
||||
.then(() => this._prepareExporter())
|
||||
.then(({ outputDir, exporter, gdjsRoot }) => {
|
||||
timeFunction(
|
||||
() => {
|
||||
const previewExportOptions = new gd.PreviewExportOptions(
|
||||
project,
|
||||
outputDir
|
||||
);
|
||||
previewExportOptions.setIsDevelopmentEnvironment(Window.isDev());
|
||||
previewExportOptions.setLayoutName(layout.getName());
|
||||
if (externalLayout) {
|
||||
previewExportOptions.setExternalLayoutName(
|
||||
externalLayout.getName()
|
||||
);
|
||||
}
|
||||
try {
|
||||
await this.getPreviewDebuggerServer().startServer();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Unable to start the Debugger Server for the preview:',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
const previewDebuggerServerAddress = getDebuggerServerAddress();
|
||||
if (previewDebuggerServerAddress) {
|
||||
previewExportOptions.useWebsocketDebuggerClientWithServerAddress(
|
||||
previewDebuggerServerAddress.address,
|
||||
'' + previewDebuggerServerAddress.port
|
||||
);
|
||||
}
|
||||
const { outputDir, exporter, gdjsRoot } = await prepareExporter();
|
||||
|
||||
const includeFileHashs = this.props.getIncludeFileHashs();
|
||||
for (const includeFile in includeFileHashs) {
|
||||
const hash = includeFileHashs[includeFile];
|
||||
previewExportOptions.setIncludeFileHash(includeFile, hash);
|
||||
}
|
||||
var previewStartTime = performance.now();
|
||||
|
||||
// Give the preview the path to the "@electron/remote" module of the editor,
|
||||
// as this is required by some features and we've not removed dependency
|
||||
// on "@electron/remote" yet.
|
||||
previewExportOptions.setElectronRemoteRequirePath(
|
||||
path.join(
|
||||
gdjsRoot,
|
||||
'../preview_node_modules',
|
||||
'@electron/remote',
|
||||
'renderer/index.js'
|
||||
)
|
||||
);
|
||||
const previewExportOptions = new gd.PreviewExportOptions(
|
||||
project,
|
||||
outputDir
|
||||
);
|
||||
previewExportOptions.setIsDevelopmentEnvironment(Window.isDev());
|
||||
previewExportOptions.setLayoutName(layout.getName());
|
||||
previewExportOptions.setIsInGameEdition(previewOptions.isForInGameEdition);
|
||||
if (externalLayout) {
|
||||
previewExportOptions.setExternalLayoutName(externalLayout.getName());
|
||||
}
|
||||
|
||||
const debuggerIds = this.getPreviewDebuggerServer().getExistingDebuggerIds();
|
||||
const shouldHotReload =
|
||||
previewOptions.hotReload && !!debuggerIds.length;
|
||||
const previewDebuggerServerAddress = getDebuggerServerAddress();
|
||||
if (previewDebuggerServerAddress) {
|
||||
previewExportOptions.useWebsocketDebuggerClientWithServerAddress(
|
||||
previewDebuggerServerAddress.address,
|
||||
'' + previewDebuggerServerAddress.port
|
||||
);
|
||||
}
|
||||
|
||||
previewExportOptions.setProjectDataOnlyExport(
|
||||
// Only export project data if asked and if a hot-reloading is being done.
|
||||
shouldHotReload && previewOptions.projectDataOnlyExport
|
||||
);
|
||||
const includeFileHashs = this.props.getIncludeFileHashs();
|
||||
for (const includeFile in includeFileHashs) {
|
||||
const hash = includeFileHashs[includeFile];
|
||||
previewExportOptions.setIncludeFileHash(includeFile, hash);
|
||||
}
|
||||
|
||||
previewExportOptions.setFullLoadingScreen(
|
||||
previewOptions.fullLoadingScreen
|
||||
);
|
||||
previewExportOptions.setGDevelopVersionWithHash(
|
||||
getIDEVersionWithHash()
|
||||
);
|
||||
previewExportOptions.setCrashReportUploadLevel(
|
||||
this.props.crashReportUploadLevel
|
||||
);
|
||||
previewExportOptions.setPreviewContext(this.props.previewContext);
|
||||
previewExportOptions.setProjectTemplateSlug(
|
||||
project.getTemplateSlug()
|
||||
);
|
||||
previewExportOptions.setSourceGameId(this.props.sourceGameId);
|
||||
// Give the preview the path to the "@electron/remote" module of the editor,
|
||||
// as this is required by some features and we've not removed dependency
|
||||
// on "@electron/remote" yet.
|
||||
previewExportOptions.setElectronRemoteRequirePath(
|
||||
path.join(
|
||||
gdjsRoot,
|
||||
'../preview_node_modules',
|
||||
'@electron/remote',
|
||||
'renderer/index.js'
|
||||
)
|
||||
);
|
||||
|
||||
if (previewOptions.fallbackAuthor) {
|
||||
previewExportOptions.setFallbackAuthor(
|
||||
previewOptions.fallbackAuthor.id,
|
||||
previewOptions.fallbackAuthor.username
|
||||
);
|
||||
}
|
||||
if (previewOptions.authenticatedPlayer) {
|
||||
previewExportOptions.setAuthenticatedPlayer(
|
||||
previewOptions.authenticatedPlayer.playerId,
|
||||
previewOptions.authenticatedPlayer.playerUsername,
|
||||
previewOptions.authenticatedPlayer.playerToken
|
||||
);
|
||||
}
|
||||
if (previewOptions.captureOptions) {
|
||||
if (previewOptions.captureOptions.screenshots) {
|
||||
previewOptions.captureOptions.screenshots.forEach(
|
||||
screenshot => {
|
||||
previewExportOptions.addScreenshotCapture(
|
||||
screenshot.delayTimeInSeconds,
|
||||
screenshot.signedUrl,
|
||||
screenshot.publicUrl
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
const debuggerIds = this.getPreviewDebuggerServer().getExistingDebuggerIds();
|
||||
const shouldHotReload = previewOptions.hotReload && !!debuggerIds.length;
|
||||
|
||||
exporter.exportProjectForPixiPreview(previewExportOptions);
|
||||
previewExportOptions.delete();
|
||||
exporter.delete();
|
||||
previewExportOptions.setProjectDataOnlyExport(
|
||||
// Only export project data if asked and if a hot-reloading is being done.
|
||||
shouldHotReload && previewOptions.projectDataOnlyExport
|
||||
);
|
||||
|
||||
if (shouldHotReload) {
|
||||
debuggerIds.forEach(debuggerId => {
|
||||
this.getPreviewDebuggerServer().sendMessage(debuggerId, {
|
||||
command: 'hotReload',
|
||||
});
|
||||
});
|
||||
previewExportOptions.setFullLoadingScreen(previewOptions.fullLoadingScreen);
|
||||
previewExportOptions.setGDevelopVersionWithHash(getIDEVersionWithHash());
|
||||
previewExportOptions.setCrashReportUploadLevel(
|
||||
this.props.crashReportUploadLevel
|
||||
);
|
||||
previewExportOptions.setPreviewContext(this.props.previewContext);
|
||||
previewExportOptions.setProjectTemplateSlug(project.getTemplateSlug());
|
||||
previewExportOptions.setSourceGameId(this.props.sourceGameId);
|
||||
|
||||
if (
|
||||
this.state.hotReloadsCount % 16 === 0 &&
|
||||
this._hotReloadSubscriptionChecker
|
||||
) {
|
||||
this._hotReloadSubscriptionChecker.checkUserHasSubscription();
|
||||
}
|
||||
this.setState(state => ({
|
||||
hotReloadsCount: state.hotReloadsCount + 1,
|
||||
}));
|
||||
} else {
|
||||
this._openPreviewWindow(project, outputDir, previewOptions);
|
||||
}
|
||||
},
|
||||
time => console.info(`Preview took ${time}ms`)
|
||||
);
|
||||
if (previewOptions.fallbackAuthor) {
|
||||
previewExportOptions.setFallbackAuthor(
|
||||
previewOptions.fallbackAuthor.id,
|
||||
previewOptions.fallbackAuthor.username
|
||||
);
|
||||
}
|
||||
if (previewOptions.authenticatedPlayer) {
|
||||
previewExportOptions.setAuthenticatedPlayer(
|
||||
previewOptions.authenticatedPlayer.playerId,
|
||||
previewOptions.authenticatedPlayer.playerUsername,
|
||||
previewOptions.authenticatedPlayer.playerToken
|
||||
);
|
||||
}
|
||||
if (previewOptions.captureOptions) {
|
||||
if (previewOptions.captureOptions.screenshots) {
|
||||
previewOptions.captureOptions.screenshots.forEach(screenshot => {
|
||||
previewExportOptions.addScreenshotCapture(
|
||||
screenshot.delayTimeInSeconds,
|
||||
screenshot.signedUrl,
|
||||
screenshot.publicUrl
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exporter.exportProjectForPixiPreview(previewExportOptions);
|
||||
previewExportOptions.delete();
|
||||
exporter.delete();
|
||||
|
||||
if (shouldHotReload) {
|
||||
debuggerIds.forEach(debuggerId => {
|
||||
this.getPreviewDebuggerServer().sendMessage(debuggerId, {
|
||||
command: 'hotReload',
|
||||
});
|
||||
});
|
||||
|
||||
if (
|
||||
this.state.hotReloadsCount % 16 === 0 &&
|
||||
this._hotReloadSubscriptionChecker
|
||||
) {
|
||||
this._hotReloadSubscriptionChecker.checkUserHasSubscription();
|
||||
}
|
||||
this.setState(state => ({
|
||||
hotReloadsCount: state.hotReloadsCount + 1,
|
||||
}));
|
||||
} else {
|
||||
if (previewOptions.isForInGameEdition) {
|
||||
attachToPreview({
|
||||
previewIndexHtmlLocation: `file://${outputDir}/index.html`,
|
||||
});
|
||||
}
|
||||
|
||||
if (previewOptions.numberOfWindows >= 1) {
|
||||
this._openPreviewWindow(project, outputDir, previewOptions);
|
||||
}
|
||||
}
|
||||
|
||||
const previewStopTime = performance.now();
|
||||
console.info(`Preview took ${previewStopTime - previewStartTime}ms`);
|
||||
};
|
||||
|
||||
getPreviewDebuggerServer() {
|
||||
|
@@ -15,6 +15,10 @@ export type LaunchPreviewOptions = {
|
||||
fullLoadingScreen?: boolean,
|
||||
forceDiagnosticReport?: boolean,
|
||||
numberOfWindows?: number,
|
||||
isForInGameEdition?: {|
|
||||
forcedSceneName: string,
|
||||
forcedExternalLayoutName: ?string,
|
||||
|},
|
||||
launchCaptureOptions?: LaunchCaptureOptions,
|
||||
};
|
||||
export type CaptureOptions = {|
|
||||
@@ -40,6 +44,7 @@ export type PreviewOptions = {|
|
||||
playerToken: string,
|
||||
},
|
||||
numberOfWindows: number,
|
||||
isForInGameEdition: boolean,
|
||||
getIsMenuBarHiddenInPreview: () => boolean,
|
||||
getIsAlwaysOnTopInPreview: () => boolean,
|
||||
captureOptions: CaptureOptions,
|
||||
@@ -59,6 +64,13 @@ export type PreviewLauncherProps = {|
|
||||
/** Each game connected to the debugger server is identified by a unique number. */
|
||||
export type DebuggerId = number;
|
||||
|
||||
/** Each game connected to the debugger server can communicate its status. */
|
||||
export type DebuggerStatus = {|
|
||||
isPaused: boolean,
|
||||
isInGameEdition: boolean,
|
||||
sceneName: string | null,
|
||||
|};
|
||||
|
||||
/** The callbacks for a debugger server used for previews. */
|
||||
export type PreviewDebuggerServerCallbacks = {|
|
||||
onErrorReceived: (err: Error) => void | Promise<void>,
|
||||
|
@@ -5,11 +5,15 @@ import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
const electron = optionalRequire('electron');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
|
||||
type Props = {
|
||||
onGDJSUpdated: () => Promise<void> | void,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up some watchers for GDJS and Extensions sources.
|
||||
* Stop the watchers when the component is unmounted or `shouldWatch` prop is false.
|
||||
*/
|
||||
export const LocalGDJSDevelopmentWatcher = () => {
|
||||
export const LocalGDJSDevelopmentWatcher = ({ onGDJSUpdated }: Props) => {
|
||||
const preferences = React.useContext(PreferencesContext);
|
||||
const shouldWatch = preferences.values.useGDJSDevelopmentWatcher;
|
||||
|
||||
@@ -35,5 +39,35 @@ export const LocalGDJSDevelopmentWatcher = () => {
|
||||
[shouldWatch]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!shouldWatch) {
|
||||
// Nothing to set up in the effect if watch is deactivated.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ipcRenderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.removeAllListeners(
|
||||
'local-gdjs-development-watcher-runtime-updated'
|
||||
);
|
||||
ipcRenderer.on(
|
||||
'local-gdjs-development-watcher-runtime-updated',
|
||||
(event, err) => {
|
||||
onGDJSUpdated();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeAllListeners(
|
||||
'local-gdjs-development-watcher-runtime-updated'
|
||||
);
|
||||
};
|
||||
},
|
||||
[shouldWatch, onGDJSUpdated]
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@@ -97,7 +97,7 @@ export const create = (authentication: Authentication) => {
|
||||
)}
|
||||
quickPublishOnlineWebExporter={localOnlineWebExporter}
|
||||
renderGDJSDevelopmentWatcher={
|
||||
isDev ? () => <LocalGDJSDevelopmentWatcher /> : null
|
||||
isDev ? ({ onGDJSUpdated }) => <LocalGDJSDevelopmentWatcher onGDJSUpdated={onGDJSUpdated} /> : null
|
||||
}
|
||||
storageProviders={storageProviders}
|
||||
resourceMover={LocalResourceMover}
|
||||
|
@@ -27,6 +27,9 @@ import {
|
||||
} from '../ResourcesWatcher';
|
||||
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
import { switchToSceneEdition } from '../../EmbeddedGame/EmbeddedGameFrame';
|
||||
|
||||
const gameEditorMode = 'embedded-game'; // TODO: move to a preference.
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
@@ -69,6 +72,13 @@ export class ExternalLayoutEditorContainer extends React.Component<
|
||||
layout ? layout.getName() : null,
|
||||
projectItemName
|
||||
);
|
||||
|
||||
if (gameEditorMode === 'embedded-game' && layout && projectItemName) {
|
||||
switchToSceneEdition({
|
||||
sceneName: layout.getName(),
|
||||
externalLayoutName: projectItemName,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.resourceExternallyChangedCallbackId = registerOnResourceExternallyChangedCallback(
|
||||
this.onResourceExternallyChanged.bind(this)
|
||||
@@ -88,6 +98,13 @@ export class ExternalLayoutEditorContainer extends React.Component<
|
||||
layout ? layout.getName() : null,
|
||||
projectItemName
|
||||
);
|
||||
|
||||
if (gameEditorMode === 'embedded-game' && layout && projectItemName) {
|
||||
switchToSceneEdition({
|
||||
sceneName: layout.getName(),
|
||||
externalLayoutName: projectItemName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,9 @@ import {
|
||||
} from './BaseEditor';
|
||||
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
import { switchToSceneEdition } from '../../EmbeddedGame/EmbeddedGameFrame';
|
||||
|
||||
const gameEditorMode = 'embedded-game'; // TODO: move to a preference.
|
||||
|
||||
export class SceneEditorContainer extends React.Component<RenderEditorContainerProps> {
|
||||
editor: ?SceneEditor;
|
||||
@@ -32,6 +35,10 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
|
||||
if (this.props.isActive) {
|
||||
const { projectItemName } = this.props;
|
||||
this.props.setPreviewedLayout(projectItemName);
|
||||
|
||||
if (gameEditorMode === 'embedded-game' && projectItemName) {
|
||||
switchToSceneEdition({ sceneName: projectItemName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +46,10 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
|
||||
if (!prevProps.isActive && this.props.isActive) {
|
||||
const { projectItemName } = this.props;
|
||||
this.props.setPreviewedLayout(projectItemName);
|
||||
|
||||
if (gameEditorMode === 'embedded-game' && projectItemName) {
|
||||
switchToSceneEdition({ sceneName: projectItemName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +155,7 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
|
||||
}
|
||||
onOpenEvents={this.props.onOpenEvents}
|
||||
isActive={isActive}
|
||||
previewDebuggerServer={this.props.previewDebuggerServer}
|
||||
hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps}
|
||||
openBehaviorEvents={this.props.openBehaviorEvents}
|
||||
onExtractAsExternalLayout={this.props.onExtractAsExternalLayout}
|
||||
|
@@ -48,6 +48,8 @@ export type EditorTab = {|
|
||||
extraEditorProps: ?EditorContainerExtraProps,
|
||||
/** If set to false, the tab can't be closed. */
|
||||
closable: boolean,
|
||||
/** If set to true, `pointer-events: none` is applied to the tab content. */
|
||||
removePointerEvents: boolean,
|
||||
|};
|
||||
|
||||
export type EditorTabsState = {|
|
||||
@@ -91,6 +93,7 @@ export type EditorOpeningOptions = {|
|
||||
extraEditorProps?: EditorContainerExtraProps,
|
||||
dontFocusTab?: boolean,
|
||||
closable?: boolean,
|
||||
removePointerEvents?: boolean,
|
||||
|};
|
||||
|
||||
export const getEditorTabMetadata = (
|
||||
@@ -138,6 +141,7 @@ export const openEditorTab = (
|
||||
key,
|
||||
extraEditorProps,
|
||||
dontFocusTab,
|
||||
removePointerEvents,
|
||||
closable,
|
||||
}: EditorOpeningOptions
|
||||
): EditorTabsState => {
|
||||
@@ -163,6 +167,7 @@ export const openEditorTab = (
|
||||
extraEditorProps,
|
||||
editorRef: null,
|
||||
closable: typeof closable === 'undefined' ? true : !!closable,
|
||||
removePointerEvents: !!removePointerEvents,
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
type PreviewDebuggerServer,
|
||||
type DebuggerId,
|
||||
type HotReloaderLog,
|
||||
type DebuggerStatus,
|
||||
} from '../ExportAndShare/PreviewLauncher.flow';
|
||||
|
||||
/** Represents what should be run when a preview is launched */
|
||||
@@ -22,26 +23,31 @@ export type PreviewState = {|
|
||||
|};
|
||||
|
||||
type PreviewDebuggerServerWatcherResults = {|
|
||||
previewDebuggerIds: Array<DebuggerId>,
|
||||
hasNonEditionPreviewsRunning: boolean,
|
||||
|
||||
hotReloadLogs: Array<HotReloaderLog>,
|
||||
clearHotReloadLogs: () => void,
|
||||
|
||||
hardReloadAllPreviews: () => void,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Return the ids of the debuggers being run, watching for changes (new
|
||||
* Return the status of the debuggers being run, watching for changes (new
|
||||
* debugger launched or existing one closed).
|
||||
*/
|
||||
export const usePreviewDebuggerServerWatcher = (
|
||||
previewDebuggerServer: ?PreviewDebuggerServer
|
||||
): PreviewDebuggerServerWatcherResults => {
|
||||
const [debuggerIds, setDebuggerIds] = React.useState<Array<DebuggerId>>([]);
|
||||
const [debuggerStatus, setDebuggerStatus] = React.useState<{
|
||||
[DebuggerId]: DebuggerStatus,
|
||||
}>({});
|
||||
const [hotReloadLogs, setHotReloadLogs] = React.useState<
|
||||
Array<HotReloaderLog>
|
||||
>([]);
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!previewDebuggerServer) {
|
||||
setDebuggerIds([]);
|
||||
setDebuggerStatus({});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,10 +56,24 @@ export const usePreviewDebuggerServerWatcher = (
|
||||
// Nothing to do.
|
||||
},
|
||||
onConnectionClosed: ({ id, debuggerIds }) => {
|
||||
setDebuggerIds([...debuggerIds]);
|
||||
// Remove the debugger status.
|
||||
setDebuggerStatus(debuggerStatus => {
|
||||
const {
|
||||
[id]: closedDebuggerStatus,
|
||||
...otherDebuggerStatus
|
||||
} = debuggerStatus;
|
||||
console.info(
|
||||
`Connection closed with preview #${id}. Last status was:`,
|
||||
closedDebuggerStatus
|
||||
);
|
||||
|
||||
return otherDebuggerStatus;
|
||||
});
|
||||
},
|
||||
onConnectionOpened: ({ id, debuggerIds }) => {
|
||||
setDebuggerIds([...debuggerIds]);
|
||||
// Ask the new debugger client for its status (but don't assume anything
|
||||
// at this stage).
|
||||
previewDebuggerServer.sendMessage(id, { command: 'getStatus' });
|
||||
},
|
||||
onConnectionErrored: ({ id }) => {
|
||||
// Nothing to do (onConnectionClosed is called if necessary).
|
||||
@@ -64,6 +84,15 @@ export const usePreviewDebuggerServerWatcher = (
|
||||
onHandleParsedMessage: ({ id, parsedMessage }) => {
|
||||
if (parsedMessage.command === 'hotReloader.logs') {
|
||||
setHotReloadLogs(parsedMessage.payload);
|
||||
} else if (parsedMessage.command === 'status') {
|
||||
setDebuggerStatus(debuggerStatus => ({
|
||||
...debuggerStatus,
|
||||
[id]: {
|
||||
isPaused: !!parsedMessage.payload.isPaused,
|
||||
isInGameEdition: !!parsedMessage.payload.isInGameEdition,
|
||||
sceneName: parsedMessage.payload.sceneName,
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -77,5 +106,28 @@ export const usePreviewDebuggerServerWatcher = (
|
||||
setHotReloadLogs,
|
||||
]);
|
||||
|
||||
return { previewDebuggerIds: debuggerIds, hotReloadLogs, clearHotReloadLogs };
|
||||
const hardReloadAllPreviews = React.useCallback(
|
||||
() => {
|
||||
if (!previewDebuggerServer) return;
|
||||
|
||||
console.info('Hard reloading all previews...');
|
||||
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
|
||||
previewDebuggerServer.sendMessage(debuggerId, {
|
||||
command: 'hardReload',
|
||||
});
|
||||
});
|
||||
},
|
||||
[previewDebuggerServer]
|
||||
);
|
||||
|
||||
const hasNonEditionPreviewsRunning = Object.keys(debuggerStatus).some(
|
||||
key => !debuggerStatus[+key].isInGameEdition
|
||||
);
|
||||
|
||||
return {
|
||||
hasNonEditionPreviewsRunning,
|
||||
hotReloadLogs,
|
||||
clearHotReloadLogs,
|
||||
hardReloadAllPreviews,
|
||||
};
|
||||
};
|
||||
|
@@ -19,7 +19,12 @@ type Props = {|
|
||||
const DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part';
|
||||
|
||||
const styles = {
|
||||
container: { display: 'flex', flexShrink: 0, alignItems: 'flex-end' },
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
alignItems: 'flex-end',
|
||||
position: 'relative',
|
||||
},
|
||||
leftSideArea: { alignSelf: 'stretch', flexShrink: 0 },
|
||||
rightSideArea: { alignSelf: 'stretch', flex: 1 },
|
||||
menuIcon: { marginLeft: 4, marginRight: 4 },
|
||||
|
@@ -87,21 +87,21 @@ const PreviewAndShareButtons = React.memo<PreviewAndShareButtonsProps>(
|
||||
click: async () => {
|
||||
await onPreviewWithoutHotReload({ numberOfWindows: 2 });
|
||||
},
|
||||
enabled: isPreviewEnabled && !hasPreviewsRunning,
|
||||
enabled: isPreviewEnabled,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`3 previews in 3 windows`),
|
||||
click: async () => {
|
||||
onPreviewWithoutHotReload({ numberOfWindows: 3 });
|
||||
},
|
||||
enabled: isPreviewEnabled && !hasPreviewsRunning,
|
||||
enabled: isPreviewEnabled,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`4 previews in 4 windows`),
|
||||
click: async () => {
|
||||
onPreviewWithoutHotReload({ numberOfWindows: 4 });
|
||||
},
|
||||
enabled: isPreviewEnabled && !hasPreviewsRunning,
|
||||
enabled: isPreviewEnabled,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -185,7 +185,9 @@ const PreviewAndShareButtons = React.memo<PreviewAndShareButtonsProps>(
|
||||
<LineStackLayout noMargin>
|
||||
<FlatButtonWithSplitMenu
|
||||
primary
|
||||
onClick={onHotReloadPreview}
|
||||
onClick={
|
||||
hasPreviewsRunning ? onHotReloadPreview : onPreviewWithoutHotReload
|
||||
}
|
||||
disabled={!isPreviewEnabled}
|
||||
icon={hasPreviewsRunning ? <UpdateIcon /> : <PreviewIcon />}
|
||||
label={
|
||||
|
@@ -200,6 +200,7 @@ import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects';
|
||||
import useGamesList from '../GameDashboard/UseGamesList';
|
||||
import useCapturesManager from './UseCapturesManager';
|
||||
import useHomepageWitchForRouting from './UseHomepageWitchForRouting';
|
||||
import { EmbeddedGameFrame } from '../EmbeddedGame/EmbeddedGameFrame';
|
||||
|
||||
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
|
||||
|
||||
@@ -305,7 +306,9 @@ export type Props = {|
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
requestUpdate?: () => void,
|
||||
renderShareDialog: ShareDialogWithoutExportsProps => React.Node,
|
||||
renderGDJSDevelopmentWatcher?: ?() => React.Node,
|
||||
renderGDJSDevelopmentWatcher?: ?({|
|
||||
onGDJSUpdated: () => Promise<void> | void,
|
||||
|}) => React.Node,
|
||||
extensionsLoader?: JsExtensionsLoader,
|
||||
initialFileMetadataToOpen: ?FileMetadata,
|
||||
initialExampleSlugToOpen: ?string,
|
||||
@@ -409,11 +412,11 @@ const MainFrame = (props: Props) => {
|
||||
_previewLauncher.current &&
|
||||
_previewLauncher.current.getPreviewDebuggerServer();
|
||||
const {
|
||||
previewDebuggerIds,
|
||||
hasNonEditionPreviewsRunning,
|
||||
hotReloadLogs,
|
||||
clearHotReloadLogs,
|
||||
hardReloadAllPreviews,
|
||||
} = usePreviewDebuggerServerWatcher(previewDebuggerServer);
|
||||
const hasPreviewsRunning = !!previewDebuggerIds.length;
|
||||
const {
|
||||
ensureInteractionHappened,
|
||||
renderOpenConfirmDialog,
|
||||
@@ -620,6 +623,10 @@ const MainFrame = (props: Props) => {
|
||||
<ExtensionIcon />
|
||||
) : null;
|
||||
|
||||
// Scene editors can have an embedded game, so they redefine manually
|
||||
// which components can have clicks/touches.
|
||||
const removePointerEvents = kind === 'layout';
|
||||
|
||||
const closable = kind !== 'start page';
|
||||
const extraEditorProps =
|
||||
kind === 'start page'
|
||||
@@ -637,6 +644,7 @@ const MainFrame = (props: Props) => {
|
||||
)
|
||||
: null,
|
||||
closable,
|
||||
removePointerEvents,
|
||||
label,
|
||||
projectItemName: name,
|
||||
tabOptions,
|
||||
@@ -1607,6 +1615,7 @@ const MainFrame = (props: Props) => {
|
||||
fullLoadingScreen,
|
||||
forceDiagnosticReport,
|
||||
launchCaptureOptions,
|
||||
isForInGameEdition,
|
||||
}: LaunchPreviewOptions) => {
|
||||
if (!currentProject) return;
|
||||
if (currentProject.getLayoutsCount() === 0) return;
|
||||
@@ -1617,10 +1626,14 @@ const MainFrame = (props: Props) => {
|
||||
setPreviewLoading(true);
|
||||
notifyPreviewOrExportWillStart(state.editorTabs);
|
||||
|
||||
const layoutName = previewState.isPreviewOverriden
|
||||
const layoutName = isForInGameEdition
|
||||
? isForInGameEdition.forcedSceneName
|
||||
: previewState.isPreviewOverriden
|
||||
? previewState.overridenPreviewLayoutName
|
||||
: previewState.previewLayoutName;
|
||||
const externalLayoutName = previewState.isPreviewOverriden
|
||||
const externalLayoutName = isForInGameEdition
|
||||
? isForInGameEdition.forcedExternalLayoutName
|
||||
: previewState.isPreviewOverriden
|
||||
? previewState.overridenPreviewExternalLayoutName
|
||||
: previewState.previewExternalLayoutName;
|
||||
|
||||
@@ -1662,6 +1675,7 @@ const MainFrame = (props: Props) => {
|
||||
const startTime = Date.now();
|
||||
await previewLauncher.launchPreview({
|
||||
project: currentProject,
|
||||
// TODO: replace by scene name and external layout name
|
||||
layout,
|
||||
externalLayout,
|
||||
networkPreview: !!networkPreview,
|
||||
@@ -1672,23 +1686,25 @@ const MainFrame = (props: Props) => {
|
||||
authenticatedPlayer,
|
||||
getIsMenuBarHiddenInPreview: preferences.getIsMenuBarHiddenInPreview,
|
||||
getIsAlwaysOnTopInPreview: preferences.getIsAlwaysOnTopInPreview,
|
||||
numberOfWindows: numberOfWindows || 1,
|
||||
numberOfWindows: numberOfWindows === undefined ? 1 : numberOfWindows,
|
||||
isForInGameEdition: !!isForInGameEdition,
|
||||
captureOptions,
|
||||
onCaptureFinished,
|
||||
});
|
||||
setPreviewLoading(false);
|
||||
|
||||
sendPreviewStarted({
|
||||
quickCustomizationGameId:
|
||||
quickCustomizationDialogOpenedFromGameId || null,
|
||||
networkPreview: !!networkPreview,
|
||||
hotReload: !!hotReload,
|
||||
projectDataOnlyExport: !!projectDataOnlyExport,
|
||||
fullLoadingScreen: !!fullLoadingScreen,
|
||||
numberOfWindows: numberOfWindows || 1,
|
||||
forceDiagnosticReport: !!forceDiagnosticReport,
|
||||
previewLaunchDuration: Date.now() - startTime,
|
||||
});
|
||||
if (!isForInGameEdition)
|
||||
sendPreviewStarted({
|
||||
quickCustomizationGameId:
|
||||
quickCustomizationDialogOpenedFromGameId || null,
|
||||
networkPreview: !!networkPreview,
|
||||
hotReload: !!hotReload,
|
||||
projectDataOnlyExport: !!projectDataOnlyExport,
|
||||
fullLoadingScreen: !!fullLoadingScreen,
|
||||
numberOfWindows: numberOfWindows || 1,
|
||||
forceDiagnosticReport: !!forceDiagnosticReport,
|
||||
previewLaunchDuration: Date.now() - startTime,
|
||||
});
|
||||
|
||||
if (inAppTutorialOrchestratorRef.current) {
|
||||
inAppTutorialOrchestratorRef.current.onPreviewLaunch();
|
||||
@@ -1772,6 +1788,47 @@ const MainFrame = (props: Props) => {
|
||||
[launchPreview]
|
||||
);
|
||||
|
||||
const onLaunchPreviewForInGameEdition = React.useCallback(
|
||||
({
|
||||
sceneName,
|
||||
externalLayoutName,
|
||||
}: {|
|
||||
sceneName: string,
|
||||
externalLayoutName: ?string,
|
||||
|}) => {
|
||||
launchPreview({
|
||||
networkPreview: false,
|
||||
hotReload: false,
|
||||
forceDiagnosticReport: false,
|
||||
isForInGameEdition: {
|
||||
forcedSceneName: sceneName,
|
||||
forcedExternalLayoutName: externalLayoutName,
|
||||
},
|
||||
numberOfWindows: 0,
|
||||
});
|
||||
},
|
||||
[launchPreview]
|
||||
);
|
||||
|
||||
const relaunchAndThenHardReloadAllPreviews = React.useCallback(
|
||||
async () => {
|
||||
// Build a new preview (so that any changes in runtime files are picked up)
|
||||
// and then ask all previews to "hard reload" themselves (i.e: refresh their page).
|
||||
await launchPreview({
|
||||
networkPreview: false,
|
||||
hotReload: false,
|
||||
forceDiagnosticReport: false,
|
||||
numberOfWindows: 0,
|
||||
});
|
||||
|
||||
hardReloadAllPreviews();
|
||||
},
|
||||
[
|
||||
hardReloadAllPreviews,
|
||||
launchPreview,
|
||||
]
|
||||
);
|
||||
|
||||
const launchQuickCustomizationPreview = React.useCallback(
|
||||
() =>
|
||||
launchPreview({
|
||||
@@ -1790,7 +1847,7 @@ const MainFrame = (props: Props) => {
|
||||
|
||||
const hotReloadPreviewButtonProps: HotReloadPreviewButtonProps = React.useMemo(
|
||||
() => ({
|
||||
hasPreviewsRunning,
|
||||
hasPreviewsRunning: hasNonEditionPreviewsRunning,
|
||||
launchProjectWithLoadingScreenPreview: () =>
|
||||
launchPreview({ fullLoadingScreen: true }),
|
||||
launchProjectDataOnlyPreview: () =>
|
||||
@@ -1798,7 +1855,7 @@ const MainFrame = (props: Props) => {
|
||||
launchProjectCodeAndDataPreview: () =>
|
||||
launchPreview({ hotReload: true, projectDataOnlyExport: false }),
|
||||
}),
|
||||
[hasPreviewsRunning, launchPreview]
|
||||
[hasNonEditionPreviewsRunning, launchPreview]
|
||||
);
|
||||
|
||||
const getEditorsTabStateWithScene = React.useCallback(
|
||||
@@ -3475,7 +3532,7 @@ const MainFrame = (props: Props) => {
|
||||
previewEnabled:
|
||||
!!state.currentProject && state.currentProject.getLayoutsCount() > 0,
|
||||
onOpenProjectManager: toggleProjectManager,
|
||||
hasPreviewsRunning,
|
||||
hasPreviewsRunning: hasNonEditionPreviewsRunning,
|
||||
allowNetworkPreview:
|
||||
!!_previewLauncher.current &&
|
||||
_previewLauncher.current.canDoNetworkPreview(),
|
||||
@@ -3570,6 +3627,30 @@ const MainFrame = (props: Props) => {
|
||||
'main-frame' /* The root styling, done in CSS to read some CSS variables. */
|
||||
}
|
||||
>
|
||||
{!!renderPreviewLauncher &&
|
||||
renderPreviewLauncher(
|
||||
{
|
||||
crashReportUploadLevel:
|
||||
preferences.values.previewCrashReportUploadLevel ||
|
||||
'exclude-javascript-code-events',
|
||||
previewContext: quickCustomizationDialogOpenedFromGameId
|
||||
? 'preview-quick-customization'
|
||||
: 'preview',
|
||||
sourceGameId: quickCustomizationDialogOpenedFromGameId || '',
|
||||
getIncludeFileHashs:
|
||||
eventsFunctionsExtensionsContext.getIncludeFileHashs,
|
||||
onExport: () => openShareDialog('publish'),
|
||||
onCaptureFinished,
|
||||
},
|
||||
(previewLauncher: ?PreviewLauncherInterface) => {
|
||||
_previewLauncher.current = previewLauncher;
|
||||
}
|
||||
)}
|
||||
<EmbeddedGameFrame
|
||||
key={currentProject ? currentProject.ptr : 0}
|
||||
previewDebuggerServer={previewDebuggerServer || null}
|
||||
onLaunchPreviewForInGameEdition={onLaunchPreviewForInGameEdition}
|
||||
/>
|
||||
{!!renderMainMenu &&
|
||||
renderMainMenu(
|
||||
{ ...buildMainMenuProps, isApplicationTopLevelMenu: true },
|
||||
@@ -3677,7 +3758,7 @@ const MainFrame = (props: Props) => {
|
||||
!checkedOutVersionStatus && !cloudProjectRecoveryOpenedVersionId
|
||||
}
|
||||
onOpenDebugger={launchDebuggerAndPreview}
|
||||
hasPreviewsRunning={hasPreviewsRunning}
|
||||
hasPreviewsRunning={hasNonEditionPreviewsRunning}
|
||||
onPreviewWithoutHotReload={launchNewPreview}
|
||||
onNetworkPreview={launchNetworkPreview}
|
||||
onHotReloadPreview={launchHotReloadPreview}
|
||||
@@ -3706,7 +3787,11 @@ const MainFrame = (props: Props) => {
|
||||
const errorBoundaryProps = getEditorErrorBoundaryProps(editorTab.key);
|
||||
|
||||
return (
|
||||
<TabContentContainer key={editorTab.key} active={isCurrentTab}>
|
||||
<TabContentContainer
|
||||
key={editorTab.key}
|
||||
active={isCurrentTab}
|
||||
removePointerEvents={editorTab.removePointerEvents}
|
||||
>
|
||||
<CommandsContextScopedProvider active={isCurrentTab}>
|
||||
<ErrorBoundary
|
||||
componentTitle={errorBoundaryProps.componentTitle}
|
||||
@@ -3837,7 +3922,11 @@ const MainFrame = (props: Props) => {
|
||||
<LoaderModal
|
||||
show={showLoader}
|
||||
progress={fileMetadataOpeningProgress}
|
||||
message={loaderModalOpeningMessage || fileMetadataOpeningMessage}
|
||||
message={
|
||||
loaderModalOpeningMessage ||
|
||||
fileMetadataOpeningMessage ||
|
||||
(previewLoading ? t`Loading preview...` : null)
|
||||
}
|
||||
/>
|
||||
<Snackbar
|
||||
open={state.snackMessageOpen}
|
||||
@@ -3860,25 +3949,6 @@ const MainFrame = (props: Props) => {
|
||||
initialTab: shareDialogInitialTab,
|
||||
gamesList,
|
||||
})}
|
||||
{!!renderPreviewLauncher &&
|
||||
renderPreviewLauncher(
|
||||
{
|
||||
crashReportUploadLevel:
|
||||
preferences.values.previewCrashReportUploadLevel ||
|
||||
'exclude-javascript-code-events',
|
||||
previewContext: quickCustomizationDialogOpenedFromGameId
|
||||
? 'preview-quick-customization'
|
||||
: 'preview',
|
||||
sourceGameId: quickCustomizationDialogOpenedFromGameId || '',
|
||||
getIncludeFileHashs:
|
||||
eventsFunctionsExtensionsContext.getIncludeFileHashs,
|
||||
onExport: () => openShareDialog('publish'),
|
||||
onCaptureFinished,
|
||||
},
|
||||
(previewLauncher: ?PreviewLauncherInterface) => {
|
||||
_previewLauncher.current = previewLauncher;
|
||||
}
|
||||
)}
|
||||
{chooseResourceOptions && onResourceChosen && !!currentProject && (
|
||||
<NewResourceDialog
|
||||
project={currentProject}
|
||||
@@ -4011,7 +4081,9 @@ const MainFrame = (props: Props) => {
|
||||
)}
|
||||
{state.gdjsDevelopmentWatcherEnabled &&
|
||||
renderGDJSDevelopmentWatcher &&
|
||||
renderGDJSDevelopmentWatcher()}
|
||||
renderGDJSDevelopmentWatcher({
|
||||
onGDJSUpdated: relaunchAndThenHardReloadAllPreviews,
|
||||
})}
|
||||
{!!hotReloadLogs.length && (
|
||||
<HotReloadLogsDialog
|
||||
logs={hotReloadLogs}
|
||||
|
@@ -307,7 +307,7 @@ const CollisionMasksEditor = ({
|
||||
if (!animations.getAnimationsCount()) return null;
|
||||
const resourceName = sprite ? sprite.getImageName() : '';
|
||||
|
||||
const editors: { [string]: Editor } = {
|
||||
const editors: { [string]: Editor | null } = {
|
||||
preview: {
|
||||
type: 'primary',
|
||||
noTitleBar: true,
|
||||
|
@@ -205,7 +205,7 @@ const PointsEditor = ({
|
||||
if (!animations.getAnimationsCount()) return null;
|
||||
const resourceName = sprite ? sprite.getImageName() : '';
|
||||
|
||||
const editors: { [string]: Editor } = {
|
||||
const editors: { [string]: Editor | null } = {
|
||||
preview: {
|
||||
type: 'primary',
|
||||
noTitleBar: true,
|
||||
|
@@ -21,6 +21,7 @@ import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/Even
|
||||
import { type TileMapTileSelection } from '../InstancesEditor/TileSetVisualizer';
|
||||
|
||||
export type SceneEditorsDisplayProps = {|
|
||||
gameEditorMode: 'embedded-game' | 'instances-editor',
|
||||
project: gdProject,
|
||||
layout: gdLayout | null,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
|
||||
@@ -38,6 +39,7 @@ export type SceneEditorsDisplayProps = {|
|
||||
multiSelect: boolean,
|
||||
targetPosition?: 'center' | 'upperCenter'
|
||||
) => void,
|
||||
onInstancesModified?: (Array<gdInitialInstance>) => void,
|
||||
editInstanceVariables: (instance: ?gdInitialInstance) => void,
|
||||
editObjectByName: (objectName: string, initialTab?: ObjectEditorTab) => void,
|
||||
editObjectInPropertiesPanel: (objectName: string) => void,
|
||||
|
@@ -82,6 +82,7 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
SceneEditorsDisplayInterface
|
||||
>((props, ref) => {
|
||||
const {
|
||||
gameEditorMode,
|
||||
project,
|
||||
resourceManagementProps,
|
||||
layout,
|
||||
@@ -95,6 +96,7 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
initialInstances,
|
||||
selectedLayer,
|
||||
onSelectInstances,
|
||||
onInstancesModified,
|
||||
} = props;
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const {
|
||||
@@ -144,6 +146,13 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
|
||||
return editorRef.current.getInstanceSize(instance);
|
||||
}, []);
|
||||
const _onInstancesModified = React.useCallback(
|
||||
instances => {
|
||||
if (onInstancesModified) onInstancesModified(instances);
|
||||
forceUpdateInstancesList();
|
||||
},
|
||||
[onInstancesModified, forceUpdateInstancesList]
|
||||
);
|
||||
const toggleEditorView = React.useCallback((editorId: EditorId) => {
|
||||
if (!editorMosaicRef.current) return;
|
||||
const config = defaultPanelConfigByEditor[editorId];
|
||||
@@ -275,7 +284,7 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
editInstanceVariables={props.editInstanceVariables}
|
||||
editObjectInPropertiesPanel={props.editObjectInPropertiesPanel}
|
||||
onEditObject={props.onEditObject}
|
||||
onInstancesModified={forceUpdateInstancesList}
|
||||
onInstancesModified={_onInstancesModified}
|
||||
onGetInstanceSize={getInstanceSize}
|
||||
ref={instanceOrObjectPropertiesEditorRef}
|
||||
unsavedChanges={props.unsavedChanges}
|
||||
@@ -323,46 +332,49 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
/>
|
||||
),
|
||||
},
|
||||
'instances-editor': {
|
||||
type: 'primary',
|
||||
noTitleBar: true,
|
||||
noSoftKeyboardAvoidance: true,
|
||||
renderEditor: () => (
|
||||
<FullSizeInstancesEditorWithScrollbars
|
||||
project={project}
|
||||
layout={layout}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
objectsContainer={objectsContainer}
|
||||
layersContainer={layersContainer}
|
||||
selectedLayer={selectedLayer}
|
||||
initialInstances={initialInstances}
|
||||
instancesEditorSettings={props.instancesEditorSettings}
|
||||
onInstancesEditorSettingsMutated={
|
||||
props.onInstancesEditorSettingsMutated
|
||||
}
|
||||
instancesSelection={props.instancesSelection}
|
||||
onInstancesAdded={props.onInstancesAdded}
|
||||
onInstancesSelected={props.onInstancesSelected}
|
||||
onInstanceDoubleClicked={props.onInstanceDoubleClicked}
|
||||
onInstancesMoved={props.onInstancesMoved}
|
||||
onInstancesResized={props.onInstancesResized}
|
||||
onInstancesRotated={props.onInstancesRotated}
|
||||
selectedObjectNames={selectedObjectNames}
|
||||
onContextMenu={props.onContextMenu}
|
||||
isInstanceOf3DObject={props.isInstanceOf3DObject}
|
||||
instancesEditorShortcutsCallbacks={
|
||||
props.instancesEditorShortcutsCallbacks
|
||||
}
|
||||
wrappedEditorRef={editor => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
pauseRendering={!props.isActive}
|
||||
tileMapTileSelection={props.tileMapTileSelection}
|
||||
onSelectTileMapTile={props.onSelectTileMapTile}
|
||||
/>
|
||||
),
|
||||
},
|
||||
'instances-editor':
|
||||
gameEditorMode === 'embedded-game'
|
||||
? null
|
||||
: {
|
||||
type: 'primary',
|
||||
noTitleBar: true,
|
||||
noSoftKeyboardAvoidance: true,
|
||||
renderEditor: () => (
|
||||
<FullSizeInstancesEditorWithScrollbars
|
||||
project={project}
|
||||
layout={layout}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
globalObjectsContainer={globalObjectsContainer}
|
||||
objectsContainer={objectsContainer}
|
||||
layersContainer={layersContainer}
|
||||
selectedLayer={selectedLayer}
|
||||
initialInstances={initialInstances}
|
||||
instancesEditorSettings={props.instancesEditorSettings}
|
||||
onInstancesEditorSettingsMutated={
|
||||
props.onInstancesEditorSettingsMutated
|
||||
}
|
||||
instancesSelection={props.instancesSelection}
|
||||
onInstancesAdded={props.onInstancesAdded}
|
||||
onInstancesSelected={props.onInstancesSelected}
|
||||
onInstanceDoubleClicked={props.onInstanceDoubleClicked}
|
||||
onInstancesMoved={props.onInstancesMoved}
|
||||
onInstancesResized={props.onInstancesResized}
|
||||
onInstancesRotated={props.onInstancesRotated}
|
||||
selectedObjectNames={selectedObjectNames}
|
||||
onContextMenu={props.onContextMenu}
|
||||
isInstanceOf3DObject={props.isInstanceOf3DObject}
|
||||
instancesEditorShortcutsCallbacks={
|
||||
props.instancesEditorShortcutsCallbacks
|
||||
}
|
||||
wrappedEditorRef={editor => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
pauseRendering={!props.isActive}
|
||||
tileMapTileSelection={props.tileMapTileSelection}
|
||||
onSelectTileMapTile={props.onSelectTileMapTile}
|
||||
/>
|
||||
),
|
||||
},
|
||||
'objects-list': {
|
||||
type: 'secondary',
|
||||
title: t`Objects`,
|
||||
@@ -448,6 +460,7 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorMosaic
|
||||
editors={editors}
|
||||
|
@@ -31,6 +31,7 @@ import getObjectByName from '../Utils/GetObjectByName';
|
||||
import UseSceneEditorCommands from './UseSceneEditorCommands';
|
||||
import { type InstancesEditorSettings } from '../InstancesEditor/InstancesEditorSettings';
|
||||
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
||||
import { type PreviewDebuggerServer } from '../ExportAndShare/PreviewLauncher.flow';
|
||||
import EditSceneIcon from '../UI/CustomSvgIcons/EditScene';
|
||||
import {
|
||||
type HistoryState,
|
||||
@@ -54,7 +55,10 @@ import { type InfoBarDetails } from '../Hints/ObjectsAdditionalWork';
|
||||
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
|
||||
import EventsRootVariablesFinder from '../Utils/EventsRootVariablesFinder';
|
||||
import { MOVEMENT_BIG_DELTA } from '../UI/KeyboardShortcuts';
|
||||
import { getInstancesInLayoutForObject } from '../Utils/Layout';
|
||||
import {
|
||||
getInstanceInLayoutWithPersistentUuid,
|
||||
getInstancesInLayoutForObject,
|
||||
} from '../Utils/Layout';
|
||||
import { zoomInFactor, zoomOutFactor } from '../Utils/ZoomUtils';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { mapFor } from '../Utils/MapFor';
|
||||
@@ -138,9 +142,11 @@ type Props = {|
|
||||
|
||||
// Preview:
|
||||
hotReloadPreviewButtonProps: HotReloadPreviewButtonProps,
|
||||
previewDebuggerServer: ?PreviewDebuggerServer,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
gameEditorMode: 'embedded-game' | 'instances-editor',
|
||||
setupGridOpen: boolean,
|
||||
scenePropertiesDialogOpen: boolean,
|
||||
layersListOpen: boolean,
|
||||
@@ -190,12 +196,14 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
contextMenu: ?ContextMenuInterface;
|
||||
editorDisplay: ?SceneEditorsDisplayInterface;
|
||||
resourceExternallyChangedCallbackId: ?string;
|
||||
unregisterDebuggerCallback: (() => void) | null = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.instancesSelection = new InstancesSelection();
|
||||
this.state = {
|
||||
gameEditorMode: 'embedded-game',
|
||||
setupGridOpen: false,
|
||||
scenePropertiesDialogOpen: false,
|
||||
layersListOpen: false,
|
||||
@@ -246,17 +254,66 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
this.resourceExternallyChangedCallbackId = registerOnResourceExternallyChangedCallback(
|
||||
this.onResourceExternallyChanged.bind(this)
|
||||
);
|
||||
|
||||
if (this.props.previewDebuggerServer) {
|
||||
this.unregisterDebuggerCallback = this.props.previewDebuggerServer.registerCallbacks(
|
||||
{
|
||||
onErrorReceived: () => {},
|
||||
onConnectionClosed: () => {},
|
||||
onConnectionOpened: () => {},
|
||||
onConnectionErrored: () => {},
|
||||
onServerStateChanged: () => {},
|
||||
onHandleParsedMessage: this.onReceiveMessageFromGame.bind(this),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
unregisterOnResourceExternallyChangedCallback(
|
||||
this.resourceExternallyChangedCallbackId
|
||||
);
|
||||
if (this.unregisterDebuggerCallback) {
|
||||
this.unregisterDebuggerCallback();
|
||||
}
|
||||
}
|
||||
|
||||
getInstancesEditorSettings() {
|
||||
return this.state.instancesEditorSettings;
|
||||
}
|
||||
|
||||
onReceiveMessageFromGame({
|
||||
id,
|
||||
parsedMessage,
|
||||
}: {
|
||||
id: number,
|
||||
parsedMessage: {| command: string, payload: any |},
|
||||
}) {
|
||||
if (parsedMessage.command === 'instances.updated') {
|
||||
if (
|
||||
!this.props.layout ||
|
||||
this.props.layout.getName() !== parsedMessage.payload.layoutName
|
||||
) {
|
||||
// TODO: handle external layout as well.
|
||||
return;
|
||||
}
|
||||
const modifiedInstances: gdInitialInstance[] = [];
|
||||
parsedMessage.payload.instances.forEach(instanceUpdateData => {
|
||||
const { persistentUuid, position } = instanceUpdateData;
|
||||
const instance = getInstanceInLayoutWithPersistentUuid(
|
||||
this.props.initialInstances,
|
||||
persistentUuid
|
||||
);
|
||||
if (!instance) return;
|
||||
instance.setX(position.x);
|
||||
instance.setY(position.y);
|
||||
instance.setZ(position.z);
|
||||
|
||||
modifiedInstances.push(instance);
|
||||
});
|
||||
this._onInstancesMoved(modifiedInstances);
|
||||
}
|
||||
}
|
||||
|
||||
onResourceExternallyChanged = async (resourceInfo: {|
|
||||
identifier: string,
|
||||
|}) => {
|
||||
@@ -812,7 +869,42 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
_exportDataOnly = debounce(() => {
|
||||
this.props.hotReloadPreviewButtonProps.launchProjectDataOnlyPreview();
|
||||
}, 250);
|
||||
|
||||
/**
|
||||
* TODO: Accept parameter that indicates which data has been modified
|
||||
* (position, rotation, size, something else?)
|
||||
*/
|
||||
_onInstancesModified = (instances: Array<gdInitialInstance>) => {
|
||||
const { previewDebuggerServer, layout } = this.props;
|
||||
if (!layout) {
|
||||
// TODO: Handle external layout
|
||||
return;
|
||||
}
|
||||
if (!previewDebuggerServer) return;
|
||||
|
||||
previewDebuggerServer.getExistingDebuggerIds().forEach(debuggerId => {
|
||||
previewDebuggerServer.sendMessage(debuggerId, {
|
||||
command: 'instances.updated',
|
||||
payload: {
|
||||
layoutName: layout.getName(),
|
||||
instances: instances.map(instance => ({
|
||||
persistentUuid: instance.getPersistentUuid(),
|
||||
position: {
|
||||
x: instance.getX(),
|
||||
y: instance.getY(),
|
||||
z: instance.getZ(),
|
||||
},
|
||||
})),
|
||||
},
|
||||
});
|
||||
});
|
||||
// TODO: Create a new export mode that will generate only the bare minimum
|
||||
// so that the runtime game has up-to-date data without unnecessary reloading scripts.
|
||||
// Once the mode exists, call it here.
|
||||
// this._exportDataOnly();
|
||||
this.forceUpdate();
|
||||
//TODO: Save for redo with debounce (and cancel on unmount)
|
||||
};
|
||||
@@ -1955,6 +2047,7 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
/>
|
||||
<EditorsDisplay
|
||||
ref={ref => (this.editorDisplay = ref)}
|
||||
gameEditorMode={this.state.gameEditorMode}
|
||||
project={project}
|
||||
layout={layout}
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
@@ -1968,6 +2061,7 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
initialInstances={initialInstances}
|
||||
instancesSelection={this.instancesSelection}
|
||||
onSelectInstances={this._onSelectInstances}
|
||||
onInstancesModified={this._onInstancesModified}
|
||||
onAddObjectInstance={this.addInstanceOnTheScene}
|
||||
selectedLayer={this.state.selectedLayer}
|
||||
editLayer={this.editLayer}
|
||||
|
@@ -45,6 +45,7 @@ const styles = {
|
||||
|
||||
type TabContentContainerProps = {|
|
||||
active: boolean,
|
||||
removePointerEvents?: boolean,
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
@@ -69,6 +70,7 @@ export class TabContentContainer extends React.Component<TabContentContainerProp
|
||||
style={{
|
||||
...styles.tabContentContainer,
|
||||
...(active ? undefined : { display: 'none' }),
|
||||
pointerEvents: this.props.removePointerEvents ? 'none' : undefined,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@@ -252,7 +252,7 @@ export type EditorMosaicInterface = {|
|
||||
type Props = {|
|
||||
initialNodes: EditorMosaicNode,
|
||||
editors: {
|
||||
[string]: Editor,
|
||||
[string]: Editor | null,
|
||||
},
|
||||
limitToOneSecondaryEditor?: boolean,
|
||||
onOpenedEditorsChanged?: () => void,
|
||||
@@ -300,7 +300,8 @@ const EditorMosaic = React.forwardRef<Props, EditorMosaicInterface>(
|
||||
if (limitToOneSecondaryEditor && editor.type === 'secondary') {
|
||||
// Replace the existing secondary editor, if any.
|
||||
const secondaryEditorName = openedEditorNames.find(
|
||||
editorName => editors[editorName].type === 'secondary'
|
||||
editorName =>
|
||||
editors[editorName] && editors[editorName].type === 'secondary'
|
||||
);
|
||||
if (secondaryEditorName) {
|
||||
setMosaicNode(
|
||||
@@ -408,14 +409,18 @@ const EditorMosaic = React.forwardRef<Props, EditorMosaicInterface>(
|
||||
// Move the entire mosaic up when the soft keyboard is open:
|
||||
'avoid-soft-keyboard': true,
|
||||
})}
|
||||
style={{ position: 'relative', width: '100%', height: '100%' }}
|
||||
renderTile={(editorName: string, path: string) => {
|
||||
const editor = editors[editorName];
|
||||
if (!editor) {
|
||||
if (editor === undefined) {
|
||||
console.error(
|
||||
'Trying to render un unknown editor: ' + editorName
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (editor === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (editor.noTitleBar) {
|
||||
return editor.renderEditor();
|
||||
|
@@ -7,10 +7,10 @@
|
||||
}
|
||||
|
||||
.mosaic-gd-theme.mosaic .mosaic-root {
|
||||
left: 0;
|
||||
/* left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
bottom: 0; */
|
||||
}
|
||||
|
||||
.mosaic-gd-theme .mosaic-window {
|
||||
|
@@ -28,14 +28,15 @@ type Props = {|
|
||||
progress?: ?number,
|
||||
|};
|
||||
|
||||
const transitionDuration = { enter: 0, exit: 150 };
|
||||
const transitionDuration = { enter: 400 };
|
||||
|
||||
const LoaderModal = ({ progress, message, show }: Props) => {
|
||||
const isInfinite = progress === null || progress === undefined;
|
||||
if (!show) return null;
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Dialog open={show} transitionDuration={transitionDuration}>
|
||||
<Dialog open transitionDuration={transitionDuration}>
|
||||
<DialogContent style={styles.dialogContent}>
|
||||
<div
|
||||
style={{
|
||||
|
@@ -1,6 +1,11 @@
|
||||
/* Mosaic layout */
|
||||
.mosaic-gd-theme.mosaic {
|
||||
background-color: var(--mosaic-layout-background-color) !important;
|
||||
/* background-color: var(--mosaic-layout-background-color) !important; */
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.mosaic-drop-target .drop-target-container {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* Mosaic window and tile */
|
||||
@@ -27,6 +32,14 @@
|
||||
background: var(--mosaic-toolbar-background-color) !important;
|
||||
}
|
||||
|
||||
.mosaic-gd-theme .mosaic-window {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.mosaic-gd-theme .mosaic-split {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.mosaic-gd-theme .mosaic-split.-column {
|
||||
background-color: var(--mosaic-layout-background-color) !important;
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ const styles = {
|
||||
overflowY: 'hidden',
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
position: 'relative',
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,30 @@
|
||||
// @flow
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
export const getInstanceInLayoutWithPersistentUuid = (
|
||||
initialInstancesContainer: gdInitialInstancesContainer,
|
||||
persistentUuid: string
|
||||
): gdInitialInstance | null => {
|
||||
if (initialInstancesContainer.getInstancesCount() === 0) return null;
|
||||
let matchingInstance = null;
|
||||
const instanceGetter = new gd.InitialInstanceJSFunctor();
|
||||
// $FlowFixMe - invoke is not writable
|
||||
instanceGetter.invoke = instancePtr => {
|
||||
// $FlowFixMe - wrapPointer is not exposed
|
||||
const instance: gdInitialInstance = gd.wrapPointer(
|
||||
instancePtr,
|
||||
gd.InitialInstance
|
||||
);
|
||||
if (instance.getPersistentUuid() === persistentUuid) {
|
||||
matchingInstance = instance;
|
||||
}
|
||||
};
|
||||
// $FlowFixMe - JSFunctor is incompatible with Functor
|
||||
initialInstancesContainer.iterateOverInstances(instanceGetter);
|
||||
instanceGetter.delete();
|
||||
return matchingInstance;
|
||||
};
|
||||
|
||||
export const getInstancesInLayoutForObject = (
|
||||
initialInstancesContainer: gdInitialInstancesContainer,
|
||||
objectName: string
|
||||
|
@@ -40,7 +40,7 @@ module.exports = {
|
||||
log.info(`Debugger connection #${id} opened.`);
|
||||
|
||||
newWebSocket.on('message', message => {
|
||||
log.info(`Debugger connection #${id} received message.`);
|
||||
// log.info(`Debugger connection #${id} received message.`);
|
||||
options.onMessage({ id, message });
|
||||
});
|
||||
|
||||
|
@@ -7,6 +7,8 @@ const process = require('process');
|
||||
const path = require('path');
|
||||
const log = require('electron-log');
|
||||
|
||||
let onRuntimeUpdatedCallback = () => {};
|
||||
|
||||
/**
|
||||
* Returns the folder corresponding to newIDE/app in **development**.
|
||||
* @returns {string}
|
||||
@@ -69,7 +71,11 @@ const onWatchEvent = debounce(
|
||||
log.info(
|
||||
`GDJS/extensions watchers found a "${eventName}" in ${resolvedFilename}, updating GDJS Runtime...`
|
||||
);
|
||||
importGDJSRuntime().catch(() => {});
|
||||
importGDJSRuntime()
|
||||
.then(() => {
|
||||
onRuntimeUpdatedCallback();
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
100 /* Avoid running the script too much in case multiple changes are fired at the same time. */
|
||||
);
|
||||
@@ -101,7 +107,7 @@ const setupLocalGDJSDevelopmentWatcher = () => {
|
||||
path.join(findDevelopmentNewIdeAppPath(), watchPath)
|
||||
);
|
||||
|
||||
// Reload extensions when the component is first mounted
|
||||
// Reload extensions when the watcher is first set.
|
||||
importGDJSRuntime().catch(() => {});
|
||||
|
||||
watcher = chokidar
|
||||
@@ -127,7 +133,12 @@ const closeLocalGDJSDevelopmentWatcher = () => {
|
||||
watcher = null;
|
||||
};
|
||||
|
||||
const onLocalGDJSDevelopmentWatcherRuntimeUpdated = cb => {
|
||||
onRuntimeUpdatedCallback = cb;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setupLocalGDJSDevelopmentWatcher,
|
||||
closeLocalGDJSDevelopmentWatcher,
|
||||
onLocalGDJSDevelopmentWatcherRuntimeUpdated,
|
||||
};
|
||||
|
@@ -30,6 +30,7 @@ const { openPreviewWindow, closePreviewWindow } = require('./PreviewWindow');
|
||||
const {
|
||||
setupLocalGDJSDevelopmentWatcher,
|
||||
closeLocalGDJSDevelopmentWatcher,
|
||||
onLocalGDJSDevelopmentWatcherRuntimeUpdated,
|
||||
} = require('./LocalGDJSDevelopmentWatcher');
|
||||
const { setupWatcher, disableWatcher } = require('./LocalFilesystemWatcher');
|
||||
|
||||
@@ -326,6 +327,11 @@ app.on('ready', function() {
|
||||
closeLocalGDJSDevelopmentWatcher();
|
||||
});
|
||||
|
||||
onLocalGDJSDevelopmentWatcherRuntimeUpdated(() => {
|
||||
log.info('Notifying the editor that the GDJS runtime has been updated.');
|
||||
mainWindow.webContents.send('local-gdjs-development-watcher-runtime-updated', null);
|
||||
});
|
||||
|
||||
// DebuggerServer events:
|
||||
ipcMain.on('debugger-start-server', (event, options) => {
|
||||
log.info('Received event to start debugger server with options=', options);
|
||||
|
Reference in New Issue
Block a user