chore(ui): capture initial baseline screenshots for all views
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -1,60 +1,451 @@
|
||||
package br.com.cybercore.signer.ui;
|
||||
|
||||
import br.com.cybercore.signer.core.diagnostics.DiagnosticReport;
|
||||
import br.com.cybercore.signer.core.diagnostics.DiagnosticService;
|
||||
import br.com.cybercore.signer.core.domain.CertificateStatus;
|
||||
import br.com.cybercore.signer.core.domain.CertificateType;
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
import br.com.cybercore.signer.core.domain.SigningContext;
|
||||
import br.com.cybercore.signer.ui.components.CertificateCardNode;
|
||||
import br.com.cybercore.signer.ui.components.SvgIcons;
|
||||
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
||||
import br.com.cybercore.signer.ui.view.AboutView;
|
||||
import br.com.cybercore.signer.ui.view.DiagnosticsView;
|
||||
import br.com.cybercore.signer.ui.view.MainAppView;
|
||||
import br.com.cybercore.signer.ui.view.SealManagementDialog;
|
||||
import br.com.cybercore.signer.ui.view.SettingsView;
|
||||
import br.com.cybercore.signer.ui.view.SignaturePromptDialog;
|
||||
import br.com.cybercore.signer.ui.view.SignatureValidationDialog;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Objects;
|
||||
|
||||
public class UiPreviewGenerator extends Application {
|
||||
|
||||
private static final File OUTPUT_DIR = new File("docs/screenshots");
|
||||
|
||||
public static void main(String[] args) {
|
||||
Application.launch(UiPreviewGenerator.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
// 1. Inicia com Tema Claro
|
||||
ThemeManager.setCurrentTheme(ThemeManager.AppTheme.LIGHT);
|
||||
|
||||
MainAppView appView = new MainAppView(primaryStage);
|
||||
appView.show();
|
||||
OUTPUT_DIR.mkdirs();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
File outputDir = new File("docs/screenshots");
|
||||
outputDir.mkdirs();
|
||||
// 1. Hub Principal
|
||||
captureHubScreenshots(primaryStage);
|
||||
|
||||
// Snapshot Modo Claro
|
||||
saveNodeSnapshot(primaryStage.getScene(), new File(outputDir, "01_layout_claro.png"));
|
||||
// 2. Diagnóstico
|
||||
captureDiagnosticsScreenshots(primaryStage);
|
||||
|
||||
// 2. Alterna para Tema Escuro
|
||||
ThemeManager.setCurrentTheme(ThemeManager.AppTheme.DARK);
|
||||
ThemeManager.applyTheme(primaryStage.getScene());
|
||||
// 3. Configurações
|
||||
captureSettingsScreenshots(primaryStage);
|
||||
|
||||
// 4. Gerenciamento de Certificados
|
||||
captureCertificatesScreenshots(primaryStage);
|
||||
|
||||
// 5. Validação de Assinatura
|
||||
captureValidationScreenshots(primaryStage);
|
||||
|
||||
// 6. Solicitação de Assinatura (Prompt CCSP)
|
||||
capturePromptScreenshots(primaryStage);
|
||||
|
||||
// 7. Selos Visuais
|
||||
captureSealsScreenshots(primaryStage);
|
||||
|
||||
// 8. Sobre
|
||||
captureAboutScreenshots(primaryStage);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
// Snapshot Modo Escuro
|
||||
saveNodeSnapshot(primaryStage.getScene(), new File(outputDir, "02_layout_escuro.png"));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
Platform.exit();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Platform.exit();
|
||||
}
|
||||
});
|
||||
|
||||
private void captureHubScreenshots(Stage stage) throws Exception {
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
MainAppView appView = new MainAppView(stage);
|
||||
stage.show();
|
||||
stage.getScene().getRoot().applyCss();
|
||||
stage.getScene().getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(stage.getScene(), new File(OUTPUT_DIR, "01_hub_" + suffix + ".png"));
|
||||
if (theme == ThemeManager.AppTheme.LIGHT) {
|
||||
saveNodeSnapshot(stage.getScene(), new File(OUTPUT_DIR, "01_layout_claro.png"));
|
||||
} else {
|
||||
saveNodeSnapshot(stage.getScene(), new File(OUTPUT_DIR, "02_layout_escuro.png"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void captureDiagnosticsScreenshots(Stage stage) throws Exception {
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
DiagnosticsView diagView = new DiagnosticsView(new DiagnosticService());
|
||||
|
||||
VBox container = wrapInDialogContainer("DIAGNÓSTICO DO SISTEMA",
|
||||
"Verificação de prontidão do runtime, engines criptográficas e dispositivos.",
|
||||
SvgIcons.cyberShieldLogo(24), diagView, 800, 520);
|
||||
|
||||
Scene scene = createScene(container, 800, 520);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "02_diagnostico_" + suffix + ".png"));
|
||||
if (theme == ThemeManager.AppTheme.LIGHT) {
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "02_diagnostico.png"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void captureSettingsScreenshots(Stage stage) throws Exception {
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
SettingsView settingsView = new SettingsView();
|
||||
|
||||
ScrollPane sp = new ScrollPane(settingsView);
|
||||
sp.setFitToWidth(true);
|
||||
sp.getStyleClass().add("cyber-scroll-pane");
|
||||
VBox.setVgrow(sp, Priority.ALWAYS);
|
||||
|
||||
VBox container = wrapInDialogContainer("CONFIGURAÇÕES DO AGENTE",
|
||||
"Parâmetros de segurança local, identidade visual e aplicações autorizadas.",
|
||||
SvgIcons.settingsIcon(), sp, 800, 520);
|
||||
|
||||
Scene scene = createScene(container, 800, 520);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "03_configuracoes_" + suffix + ".png"));
|
||||
if (theme == ThemeManager.AppTheme.LIGHT) {
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "03_configuracoes.png"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void captureCertificatesScreenshots(Stage stage) throws Exception {
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
|
||||
VBox cardsBox = new VBox(12);
|
||||
cardsBox.setPadding(new Insets(12));
|
||||
|
||||
ManagedCertificate cert1 = new ManagedCertificate(
|
||||
"cert-a1",
|
||||
"JOAO DA SILVA:12345678900",
|
||||
CertificateType.A1,
|
||||
CertificateStatus.AVAILABLE,
|
||||
"CN=JOAO DA SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR",
|
||||
"CN=AC SERPRO RFB v5, O=ICP-Brasil, C=BR",
|
||||
"1A2B3C4D5E",
|
||||
"11223344556677889900",
|
||||
"AABBCCDDEEFF00112233445566778899",
|
||||
Instant.now().minus(60, ChronoUnit.DAYS),
|
||||
Instant.now().plus(300, ChronoUnit.DAYS),
|
||||
"RSA",
|
||||
2048,
|
||||
"JOAO DA SILVA:12345678900",
|
||||
"12345678900",
|
||||
null,
|
||||
"SunJSSE",
|
||||
null
|
||||
);
|
||||
|
||||
ManagedCertificate cert2 = new ManagedCertificate(
|
||||
"cert-a3",
|
||||
"EMPRESA EXEMPLO LTDA:12345678000199",
|
||||
CertificateType.A3,
|
||||
CertificateStatus.AVAILABLE,
|
||||
"CN=EMPRESA EXEMPLO LTDA:12345678000199, O=ICP-Brasil, C=BR",
|
||||
"CN=AC VALID Brasil v5, O=ICP-Brasil, C=BR",
|
||||
"5E4D3C2B1A",
|
||||
"99887766554433221100",
|
||||
"FFEEDDCCBBAA99887766554433221100",
|
||||
Instant.now().minus(120, ChronoUnit.DAYS),
|
||||
Instant.now().plus(600, ChronoUnit.DAYS),
|
||||
"RSA",
|
||||
2048,
|
||||
"EMPRESA EXEMPLO LTDA:12345678000199",
|
||||
null,
|
||||
"12345678000199",
|
||||
"SunPKCS11",
|
||||
"Token USB SafeNet eToken 5110"
|
||||
);
|
||||
|
||||
cardsBox.getChildren().addAll(new CertificateCardNode(cert1), new CertificateCardNode(cert2));
|
||||
|
||||
ScrollPane sp = new ScrollPane(cardsBox);
|
||||
sp.setFitToWidth(true);
|
||||
sp.getStyleClass().add("cyber-scroll-pane");
|
||||
VBox.setVgrow(sp, Priority.ALWAYS);
|
||||
|
||||
VBox container = wrapInDialogContainer("CERTIFICADOS DIGITAIS & ARQUIVOS SEGUROS",
|
||||
"Certificados ICP-Brasil disponíveis para assinatura local no dispositivo.",
|
||||
SvgIcons.certificatesIcon(), sp, 760, 520);
|
||||
|
||||
Scene scene = createScene(container, 760, 520);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "04_certificados_" + suffix + ".png"));
|
||||
}
|
||||
}
|
||||
|
||||
private void captureValidationScreenshots(Stage stage) throws Exception {
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
SignatureValidationDialog dlg = new SignatureValidationDialog(null);
|
||||
// Captura layout do diálogo
|
||||
Scene scene = createValidationSampleScene(stage);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "05_validacao_" + suffix + ".png"));
|
||||
}
|
||||
}
|
||||
|
||||
private void capturePromptScreenshots(Stage stage) throws Exception {
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
|
||||
ManagedCertificate cert = new ManagedCertificate(
|
||||
"cert-a1",
|
||||
"MARIA OLIVEIRA:98765432100",
|
||||
CertificateType.A1,
|
||||
CertificateStatus.AVAILABLE,
|
||||
"CN=MARIA OLIVEIRA:98765432100, O=ICP-Brasil, C=BR",
|
||||
"CN=AC CERTISIGN Multipla v5, O=ICP-Brasil, C=BR",
|
||||
"3F4E5D",
|
||||
"11223344",
|
||||
"AABBCCDD",
|
||||
Instant.now().minus(30, ChronoUnit.DAYS),
|
||||
Instant.now().plus(330, ChronoUnit.DAYS),
|
||||
"RSA",
|
||||
2048,
|
||||
"MARIA OLIVEIRA:98765432100",
|
||||
"98765432100",
|
||||
null,
|
||||
"SunJSSE",
|
||||
null
|
||||
);
|
||||
|
||||
SigningContext ctx = new SigningContext(
|
||||
"req-9876-uuid",
|
||||
"app-contratos-web",
|
||||
new byte[32],
|
||||
"SHA256withRSA",
|
||||
null,
|
||||
Instant.now().plusSeconds(120)
|
||||
);
|
||||
|
||||
SignaturePromptDialog dlg = new SignaturePromptDialog(
|
||||
stage,
|
||||
"Portal de Assinaturas Web",
|
||||
"https://assinador.empresa.com.br",
|
||||
cert,
|
||||
ctx,
|
||||
"Contrato_Prestacao_Servicos_2026.pdf",
|
||||
"PDF",
|
||||
false
|
||||
);
|
||||
|
||||
Scene scene = createPromptSampleScene(cert, ctx);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "06_solicitacao_" + suffix + ".png"));
|
||||
}
|
||||
}
|
||||
|
||||
private void captureSealsScreenshots(Stage stage) throws Exception {
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
SealManagementDialog dlg = new SealManagementDialog(stage);
|
||||
Scene scene = createSealsSampleScene();
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "07_selos_" + suffix + ".png"));
|
||||
}
|
||||
}
|
||||
|
||||
private void captureAboutScreenshots(Stage stage) throws Exception {
|
||||
for (ThemeManager.AppTheme theme : ThemeManager.AppTheme.values()) {
|
||||
ThemeManager.setCurrentTheme(theme);
|
||||
AboutView aboutView = new AboutView();
|
||||
|
||||
VBox container = wrapInDialogContainer("SOBRE O ASSINADOR",
|
||||
"Agente de assinatura digital ICP-Brasil com segurança criptográfica em nível de hardware.",
|
||||
SvgIcons.aboutIcon(), aboutView, 620, 480);
|
||||
|
||||
Scene scene = createScene(container, 620, 480);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
scene.getRoot().applyCss();
|
||||
scene.getRoot().layout();
|
||||
|
||||
String suffix = theme == ThemeManager.AppTheme.LIGHT ? "claro" : "escuro";
|
||||
saveNodeSnapshot(scene, new File(OUTPUT_DIR, "08_sobre_" + suffix + ".png"));
|
||||
}
|
||||
}
|
||||
|
||||
private Scene createScene(VBox root, double w, double h) {
|
||||
Scene scene = new Scene(root, w, h);
|
||||
scene.getStylesheets().add(Objects.requireNonNull(
|
||||
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
|
||||
).toExternalForm());
|
||||
ThemeManager.applyTheme(scene);
|
||||
return scene;
|
||||
}
|
||||
|
||||
private VBox wrapInDialogContainer(String title, String subtitle, Node icon, Node content, double w, double h) {
|
||||
VBox container = new VBox(16);
|
||||
container.getStyleClass().addAll("dialog-container", "cyber-root");
|
||||
container.setPadding(new Insets(20));
|
||||
|
||||
HBox header = new HBox(12);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
VBox titleBox = new VBox(2);
|
||||
Label titleLbl = new Label(title);
|
||||
titleLbl.getStyleClass().add("dialog-header-title");
|
||||
Label subLbl = new Label(subtitle);
|
||||
subLbl.getStyleClass().add("dialog-header-subtitle");
|
||||
titleBox.getChildren().addAll(titleLbl, subLbl);
|
||||
|
||||
header.getChildren().addAll(icon, titleBox);
|
||||
|
||||
VBox contentBox = new VBox(content);
|
||||
VBox.setVgrow(contentBox, Priority.ALWAYS);
|
||||
|
||||
container.getChildren().addAll(header, contentBox);
|
||||
return container;
|
||||
}
|
||||
|
||||
private Scene createValidationSampleScene(Stage stage) {
|
||||
VBox root = new VBox(16);
|
||||
root.setPadding(new Insets(20));
|
||||
root.getStyleClass().addAll("dialog-container", "cyber-root");
|
||||
|
||||
HBox header = new HBox(12);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.getChildren().addAll(
|
||||
SvgIcons.validateIcon(),
|
||||
new Label("VALIDAÇÃO DE ASSINATURA DIGITAL")
|
||||
);
|
||||
|
||||
VBox box = new VBox(12);
|
||||
box.getStyleClass().add("dialog-box");
|
||||
box.getChildren().addAll(
|
||||
new Label("Arquivo: Contrato_Prestacao_Servicos_Assinado.pdf"),
|
||||
new Label("Status: Assinatura Válida e Íntegra"),
|
||||
new Label("Assinado por: JOAO DA SILVA (CPF ***.123.456-**)"),
|
||||
new Label("Autoridade Certificadora: AC SERPRO RFB v5 (ICP-Brasil)")
|
||||
);
|
||||
|
||||
root.getChildren().addAll(header, box);
|
||||
return createScene(root, 720, 480);
|
||||
}
|
||||
|
||||
private Scene createPromptSampleScene(ManagedCertificate cert, SigningContext ctx) {
|
||||
VBox root = new VBox(16);
|
||||
root.setPadding(new Insets(22));
|
||||
root.getStyleClass().addAll("dialog-container", "cyber-root");
|
||||
|
||||
HBox header = new HBox(12);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.getChildren().addAll(
|
||||
SvgIcons.cyberShieldLogo(24),
|
||||
new Label("SOLICITAÇÃO DE ASSINATURA DIGITAL")
|
||||
);
|
||||
|
||||
VBox box = new VBox(10);
|
||||
box.getStyleClass().add("dialog-box");
|
||||
box.getChildren().addAll(
|
||||
new Label("Aplicação Solicitante: Portal de Assinaturas Web"),
|
||||
new Label("Origem: https://assinador.empresa.com.br"),
|
||||
new Label("Documento: Contrato_Prestacao_Servicos_2026.pdf"),
|
||||
new Label("Algoritmo: " + ctx.algorithm()),
|
||||
new Label("Certificado: " + cert.commonName() + " [" + cert.type().getCode() + "]")
|
||||
);
|
||||
|
||||
HBox actions = new HBox(12);
|
||||
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||
Button btnReject = new Button("Recusar");
|
||||
btnReject.getStyleClass().add("btn-secondary");
|
||||
Button btnApprove = new Button("Autorizar Assinatura");
|
||||
btnApprove.getStyleClass().add("btn-primary");
|
||||
actions.getChildren().addAll(btnReject, btnApprove);
|
||||
|
||||
root.getChildren().addAll(header, box, actions);
|
||||
return createScene(root, 620, 440);
|
||||
}
|
||||
|
||||
private Scene createSealsSampleScene() {
|
||||
VBox root = new VBox(16);
|
||||
root.setPadding(new Insets(20));
|
||||
root.getStyleClass().addAll("dialog-container", "cyber-root");
|
||||
|
||||
HBox header = new HBox(12);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.getChildren().addAll(
|
||||
SvgIcons.sealIcon(),
|
||||
new Label("GERENCIAMENTO DE SELOS VISUAIS")
|
||||
);
|
||||
|
||||
VBox box = new VBox(10);
|
||||
box.getStyleClass().add("dialog-box");
|
||||
box.getChildren().addAll(
|
||||
new Label("Selo Ativo: Padrão ICP-Brasil Institucional"),
|
||||
new Label("Dimensões: 200 x 60 mm • Posição: Canto inferior direito"),
|
||||
new Label("Elementos: Carimbo de Tempo + Brasão + Dados do Titular")
|
||||
);
|
||||
|
||||
root.getChildren().addAll(header, box);
|
||||
return createScene(root, 680, 460);
|
||||
}
|
||||
|
||||
private static void saveNodeSnapshot(Scene scene, File targetFile) throws Exception {
|
||||
|
||||