From 0d60fe750fa80a0da63c0d17a8b6c05a48a5e32b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 20:43:00 +0000 Subject: [PATCH 1/3] feat: add Save button to PASCAL EDITOR to export layout JSON - Added a `SaveButton` component to `viewer-toolbar.tsx`. - Integrated it into `ViewerToolbarRight`. - The button uses the central `useScene` state to extract `nodes` and `rootNodeIds`, formats them into a JSON Blob, and triggers a file download for layout preservation. - Uses standard toolbar styling and the lucide-react 'Save' icon. Co-authored-by: Luis-Dokkaebi <26320381+Luis-Dokkaebi@users.noreply.github.com> --- .../src/components/ui/viewer-toolbar.tsx | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/components/ui/viewer-toolbar.tsx b/packages/editor/src/components/ui/viewer-toolbar.tsx index 53fe873eef..836b967107 100644 --- a/packages/editor/src/components/ui/viewer-toolbar.tsx +++ b/packages/editor/src/components/ui/viewer-toolbar.tsx @@ -1,8 +1,9 @@ 'use client' import { Icon as IconifyIcon } from '@iconify/react' +import { useScene } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { Check, ChevronsLeft, ChevronsRight, Columns2, Eye, Footprints, Moon, Sun } from 'lucide-react' +import { Check, ChevronsLeft, ChevronsRight, Columns2, Eye, Footprints, Moon, Sun, Save } from 'lucide-react' import { useCallback } from 'react' import { cn } from '../../lib/utils' import useEditor from '../../store/use-editor' @@ -366,6 +367,39 @@ function PreviewButton() { ) } +function SaveButton() { + const nodes = useScene((state) => state.nodes) + const rootNodeIds = useScene((state) => state.rootNodeIds) + + const handleSaveBuild = () => { + const sceneData = { nodes, rootNodeIds } + const json = JSON.stringify(sceneData, null, 2) + const blob = new Blob([json], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + const date = new Date().toISOString().split('T')[0] + link.download = `layout_${date}.json` + link.click() + URL.revokeObjectURL(url) + } + + return ( + + + + + Save Build + + ) +} + // ── Composed toolbar sections ─────────────────────────────────────────────── export function ViewerToolbarLeft() { @@ -390,6 +424,8 @@ export function ViewerToolbarRight() {
+
+
) } From 0a0c7bf06343b1520bc88a7469bc8df899e986af Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 03:11:09 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20add=20Holtmont=20=E2=86=94=20editor?= =?UTF-8?q?=20postMessage=20bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementa el protocolo completo de comunicación iframe para integrar el editor Pascal dentro de REAL-HOLTMONT (Vue 3). Protocolo: - PASCAL_READY: emitido al montar el listener para eliminar carreras de tiempo con el padre. - HOLTMONT_3D_IMPORT: aplica la escena recibida al store via setScene() (migrations + orphan cleanup + dirty marks). Collections se restauran con setState si vienen no vacías. - HOLTMONT_3D_IMPORT_ACK: enviado al padre tras aplicar la escena con éxito, para detener los reintentos cada 800ms. - HOLTMONT_3D_EXPORT: botón "Guardar en Holtmont" en ViewerToolbarLeft que envía {nodes, rootNodeIds, collections}. Archivos añadidos/modificados: - apps/editor/app/holtmont-bridge.tsx (nuevo — cliente) - apps/editor/app/layout.tsx (monta HoltmontBridge) - apps/editor/app/test-holtmont/page.tsx (página de prueba) - packages/editor/src/components/ui/HoltmontExportButton.tsx (nuevo) - packages/editor/src/components/ui/viewer-toolbar.tsx (ViewerToolbarLeft) https://claude.ai/code/session_01T8wymt2MFFyrZ61TzFNg5b --- apps/editor/app/holtmont-bridge.tsx | 68 ++++++ apps/editor/app/layout.tsx | 2 + apps/editor/app/test-holtmont/page.tsx | 220 ++++++++++++++++++ .../components/ui/HoltmontExportButton.tsx | 35 +++ .../src/components/ui/viewer-toolbar.tsx | 4 + 5 files changed, 329 insertions(+) create mode 100644 apps/editor/app/holtmont-bridge.tsx create mode 100644 apps/editor/app/test-holtmont/page.tsx create mode 100644 packages/editor/src/components/ui/HoltmontExportButton.tsx diff --git a/apps/editor/app/holtmont-bridge.tsx b/apps/editor/app/holtmont-bridge.tsx new file mode 100644 index 0000000000..9114e9aedb --- /dev/null +++ b/apps/editor/app/holtmont-bridge.tsx @@ -0,0 +1,68 @@ +'use client' + +import { useScene } from '@pascal-app/core' +import { useEffect } from 'react' + +export function HoltmontBridge() { + useEffect(() => { + const isIframe = window.parent !== window.self + + function postToParent(payload: Record) { + if (isIframe) { + window.parent.postMessage(payload, '*') + } + } + + function handleMessage(event: MessageEvent) { + const data = event.data as Record | null + if (!data || data.type !== 'HOLTMONT_3D_IMPORT') return + + try { + const projectData = data.projectData as Record | undefined + if (!projectData?.nodes) { + console.warn('[HoltmontBridge] HOLTMONT_3D_IMPORT: missing projectData.nodes — ignored') + return + } + + const nodes = projectData.nodes as Record + const rootNodeIds = Array.isArray(projectData.rootNodeIds) + ? (projectData.rootNodeIds as string[]) + : [] + const collections = + projectData.collections && typeof projectData.collections === 'object' + ? (projectData.collections as Record) + : {} + + console.log('[HoltmontBridge] Received HOLTMONT_3D_IMPORT:', { + nodeCount: Object.keys(nodes).length, + rootNodeIds, + }) + + // setScene runs migrations, removes orphans, marks all nodes dirty, notifies subscribers + useScene.getState().setScene(nodes as any, rootNodeIds) + + // setScene always resets collections to {}; restore them if present + if (Object.keys(collections).length > 0) { + useScene.setState({ collections: collections as any }) + } + + console.log('[HoltmontBridge] Scene applied — sending HOLTMONT_3D_IMPORT_ACK') + postToParent({ type: 'HOLTMONT_3D_IMPORT_ACK' }) + } catch (err) { + console.error('[HoltmontBridge] Failed to apply imported scene:', err) + } + } + + window.addEventListener('message', handleMessage) + + // Signal to parent that the editor is ready to receive scenes + postToParent({ type: 'PASCAL_READY' }) + console.log('[HoltmontBridge] Listener mounted — PASCAL_READY sent') + + return () => { + window.removeEventListener('message', handleMessage) + } + }, []) + + return null +} diff --git a/apps/editor/app/layout.tsx b/apps/editor/app/layout.tsx index 5257d59bf1..93e7b5b2e2 100644 --- a/apps/editor/app/layout.tsx +++ b/apps/editor/app/layout.tsx @@ -4,6 +4,7 @@ import { Barlow } from 'next/font/google' import localFont from 'next/font/local' import Script from 'next/script' import './globals.css' +import { HoltmontBridge } from './holtmont-bridge' const geistSans = localFont({ src: './fonts/GeistVF.woff', @@ -41,6 +42,7 @@ export default function RootLayout({ )} + {children} {process.env.NODE_ENV === 'development' && } diff --git a/apps/editor/app/test-holtmont/page.tsx b/apps/editor/app/test-holtmont/page.tsx new file mode 100644 index 0000000000..6b36f5bb90 --- /dev/null +++ b/apps/editor/app/test-holtmont/page.tsx @@ -0,0 +1,220 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +// Minimal test scene: Site → Building → Level → 4 walls + slab + ceiling +const TEST_SCENE = { + nodes: { + 'site-test': { + object: 'node', + id: 'site-test', + type: 'site', + parentId: null, + visible: true, + metadata: {}, + position: [0, 0, 0], + rotation: 0, + children: ['building-test'], + }, + 'building-test': { + object: 'node', + id: 'building-test', + type: 'building', + parentId: 'site-test', + visible: true, + metadata: {}, + position: [0, 0, 0], + rotation: 0, + children: ['level-test'], + }, + 'level-test': { + object: 'node', + id: 'level-test', + type: 'level', + parentId: 'building-test', + visible: true, + metadata: {}, + position: [0, 0, 0], + rotation: 0, + level: 0, + children: ['wall-n', 'wall-s', 'wall-e', 'wall-w', 'slab-test', 'ceiling-test'], + }, + 'wall-n': { + object: 'node', + id: 'wall-n', + type: 'wall', + parentId: 'level-test', + visible: true, + metadata: {}, + start: [-4, 0], + end: [4, 0], + height: 3, + thickness: 0.2, + children: [], + }, + 'wall-s': { + object: 'node', + id: 'wall-s', + type: 'wall', + parentId: 'level-test', + visible: true, + metadata: {}, + start: [4, -6], + end: [-4, -6], + height: 3, + thickness: 0.2, + children: [], + }, + 'wall-e': { + object: 'node', + id: 'wall-e', + type: 'wall', + parentId: 'level-test', + visible: true, + metadata: {}, + start: [4, 0], + end: [4, -6], + height: 3, + thickness: 0.2, + children: [], + }, + 'wall-w': { + object: 'node', + id: 'wall-w', + type: 'wall', + parentId: 'level-test', + visible: true, + metadata: {}, + start: [-4, -6], + end: [-4, 0], + height: 3, + thickness: 0.2, + children: [], + }, + 'slab-test': { + object: 'node', + id: 'slab-test', + type: 'slab', + parentId: 'level-test', + visible: true, + metadata: {}, + position: [0, 0, 0], + rotation: 0, + points: [ + [-4, 0], + [4, 0], + [4, -6], + [-4, -6], + ], + thickness: 0.2, + children: [], + }, + 'ceiling-test': { + object: 'node', + id: 'ceiling-test', + type: 'ceiling', + parentId: 'level-test', + visible: true, + metadata: {}, + position: [0, 0, 0], + rotation: 0, + points: [ + [-4, 0], + [4, 0], + [4, -6], + [-4, -6], + ], + thickness: 0.1, + height: 3, + children: [], + }, + }, + rootNodeIds: ['site-test'], + collections: {}, +} + +export default function TestHoltmontPage() { + const iframeRef = useRef(null) + const [log, setLog] = useState([]) + const [ready, setReady] = useState(false) + + const appendLog = (msg: string) => + setLog((prev) => [`${new Date().toISOString().slice(11, 23)} ${msg}`, ...prev].slice(0, 50)) + + useEffect(() => { + const handler = (event: MessageEvent) => { + const data = event.data as Record | null + if (!data?.type) return + + if (data.type === 'PASCAL_READY') { + appendLog('← PASCAL_READY received') + setReady(true) + } else if (data.type === 'HOLTMONT_3D_IMPORT_ACK') { + appendLog('← HOLTMONT_3D_IMPORT_ACK received') + } else if (data.type === 'HOLTMONT_3D_EXPORT') { + const exportData = data.data as Record + appendLog( + `← HOLTMONT_3D_EXPORT received (${Object.keys(exportData?.nodes ?? {}).length} nodes)`, + ) + } + } + + window.addEventListener('message', handler) + return () => window.removeEventListener('message', handler) + }, []) + + const sendScene = () => { + const iframe = iframeRef.current + if (!iframe?.contentWindow) return + appendLog('→ Sending HOLTMONT_3D_IMPORT...') + iframe.contentWindow.postMessage({ type: 'HOLTMONT_3D_IMPORT', projectData: TEST_SCENE }, '*') + } + + const sendAgain = () => { + setReady(false) + sendScene() + } + + return ( +
+
+

Holtmont Bridge Test

+ +
+
+ Editor status: {ready ? '✓ READY' : 'waiting…'} +
+ + +
+ +
Log (newest first):
+
+ {log.map((entry, i) => ( +
+ {entry} +
+ ))} +
+
+ +