-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Add Save button and Holtmont messaging bridge to Pascal Editor #786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Luis-Dokkaebi
wants to merge
6
commits into
pascalorg:main
Choose a base branch
from
Luis-Dokkaebi:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0d60fe7
feat: add Save button to PASCAL EDITOR to export layout JSON
google-labs-jules[bot] 9ab702b
Merge pull request #1 from Luis-Dokkaebi/add-save-button-538352697845…
Luis-Dokkaebi 0a0c7bf
feat: add Holtmont ↔ editor postMessage bridge
claude 21db2b0
Merge pull request #2 from Luis-Dokkaebi/claude/pascal-holtmont-messa…
Luis-Dokkaebi cd4936e
El plano 3D se dibuja: valida lo que llega y detecta bien el backend
claude 9a47735
Merge pull request #3 from Luis-Dokkaebi/claude/prework-order-3d-dark…
Luis-Dokkaebi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| 'use client' | ||
|
|
||
| import { sceneRegistry, useScene } from '@pascal-app/core' | ||
| import { CATALOG_ITEMS } from '@pascal-app/editor' | ||
| import { useEffect } from 'react' | ||
| import { Box3 } from 'three' | ||
| import { HoltmontImportError, normalizeHoltmontScene } from '../lib/holtmont-import' | ||
|
|
||
| export function HoltmontBridge() { | ||
| useEffect(() => { | ||
| const isIframe = window.parent !== window.self | ||
|
|
||
| function postToParent(payload: Record<string, unknown>) { | ||
| if (isIframe) { | ||
| window.parent.postMessage(payload, '*') | ||
| } | ||
| } | ||
|
|
||
| function handleMessage(event: MessageEvent) { | ||
| const data = event.data as Record<string, unknown> | null | ||
| if (!data || data.type !== 'HOLTMONT_3D_IMPORT') return | ||
|
|
||
| let scene: ReturnType<typeof normalizeHoltmontScene> | ||
| try { | ||
| // Valida contra el esquema real del editor antes de tocar el store: un | ||
| // nodo mal formado revienta después, dentro del bucle de render, y ahí | ||
| // ya no hay forma de avisar — solo queda el lienzo en negro. | ||
| scene = normalizeHoltmontScene(data.projectData, CATALOG_ITEMS) | ||
| } catch (err) { | ||
| const reason = err instanceof HoltmontImportError ? err.message : String(err) | ||
| console.error('[HoltmontBridge] HOLTMONT_3D_IMPORT rechazado:', reason) | ||
| // La escena que ya estaba montada se queda como está: sustituirla por | ||
| // una vacía convierte un import fallido en una pantalla negra. | ||
| postToParent({ type: 'HOLTMONT_3D_IMPORT_ERROR', reason, dropped: [] }) | ||
| return | ||
| } | ||
|
|
||
| try { | ||
| console.log('[HoltmontBridge] HOLTMONT_3D_IMPORT recibido:', { | ||
| nodeCount: Object.keys(scene.nodes).length, | ||
| rootNodeIds: scene.rootNodeIds, | ||
| dropped: scene.dropped, | ||
| }) | ||
|
|
||
| // setScene corre migraciones, quita huérfanos, marca todo sucio y avisa | ||
| // a los suscriptores. | ||
| useScene.getState().setScene(scene.nodes, scene.rootNodeIds) | ||
|
|
||
| // setScene siempre deja collections en {}; se restauran si venían. | ||
| if (Object.keys(scene.collections).length > 0) { | ||
| useScene.setState({ collections: scene.collections as never }) | ||
| } | ||
|
|
||
| postToParent({ | ||
| type: 'HOLTMONT_3D_IMPORT_ACK', | ||
| nodeCount: Object.keys(scene.nodes).length, | ||
| dropped: scene.dropped, | ||
| }) | ||
| } catch (err) { | ||
| console.error('[HoltmontBridge] No se pudo aplicar la escena importada:', err) | ||
| postToParent({ | ||
| type: 'HOLTMONT_3D_IMPORT_ERROR', | ||
| reason: String(err), | ||
| dropped: scene.dropped, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| window.addEventListener('message', handleMessage) | ||
|
|
||
| // Sonda de diagnóstico: dice qué se dibujó de verdad, no qué se guardó. | ||
| // La usa la prueba de humo (`scripts/holtmont-smoke.mjs`) para distinguir | ||
| // «la escena está en el store» de «la escena tiene geometría en pantalla», | ||
| // que es justo la diferencia entre el bug del lienzo negro y el arreglo. | ||
| ;(window as unknown as Record<string, unknown>).__holtmontProbe = () => { | ||
| const nodes = useScene.getState().nodes | ||
| const resumen: Record<string, { total: number; conGeometria: number }> = {} | ||
| let mayorLado = 0 | ||
|
|
||
| for (const [tipo, ids] of Object.entries(sceneRegistry.byType)) { | ||
| const entrada = { total: 0, conGeometria: 0 } | ||
| for (const id of ids) { | ||
| if (!nodes[id as keyof typeof nodes]) continue | ||
| entrada.total += 1 | ||
| const objeto = sceneRegistry.nodes.get(id) | ||
| if (!objeto) continue | ||
| const caja = new Box3().setFromObject(objeto) | ||
| if (caja.isEmpty()) continue | ||
| const lado = Math.max( | ||
| caja.max.x - caja.min.x, | ||
| caja.max.y - caja.min.y, | ||
| caja.max.z - caja.min.z, | ||
| ) | ||
| if (lado > 0.01) { | ||
| entrada.conGeometria += 1 | ||
| mayorLado = Math.max(mayorLado, lado) | ||
| } | ||
| } | ||
| if (entrada.total > 0) resumen[tipo] = entrada | ||
| } | ||
|
|
||
| return { nodos: Object.keys(nodes).length, porTipo: resumen, mayorLado } | ||
| } | ||
|
|
||
| // Avisa al padre que el editor ya puede recibir escenas. | ||
| postToParent({ type: 'PASCAL_READY' }) | ||
| console.log('[HoltmontBridge] Listener montado — PASCAL_READY enviado') | ||
|
|
||
| return () => { | ||
| window.removeEventListener('message', handleMessage) | ||
| } | ||
| }, []) | ||
|
|
||
| return null | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| 'use client' | ||
|
|
||
| /** | ||
| * Banco de pruebas del puente con Holtmont. | ||
| * | ||
| * Carga las escenas que genera el agente (`public/holtmont-fixtures/`, escritas | ||
| * por `scripts/generar_escenas_pascal.py` del repositorio HOLTMONT-PYTHON) y las | ||
| * manda al editor por el mismo `postMessage` que usa la Pre Work Order. Sirve a | ||
| * mano y también para la prueba de humo con navegador | ||
| * (`scripts/holtmont-smoke.mjs`), que entra con `?scene=<nombre>` y lee el | ||
| * resultado de `window.__holtmontTest`. | ||
| */ | ||
|
|
||
| import { useCallback, useEffect, useRef, useState } from 'react' | ||
|
|
||
| type Registro = { hora: string; texto: string; entrante: boolean } | ||
|
|
||
| type EstadoDePrueba = { | ||
| ready: boolean | ||
| ack: Record<string, unknown> | null | ||
| error: Record<string, unknown> | null | ||
| enviada: string | null | ||
| } | ||
|
|
||
| declare global { | ||
| interface Window { | ||
| __holtmontTest?: EstadoDePrueba | ||
| } | ||
| } | ||
|
|
||
| export default function TestHoltmontPage() { | ||
| const iframeRef = useRef<HTMLIFrameElement>(null) | ||
| const [log, setLog] = useState<Registro[]>([]) | ||
| const [ready, setReady] = useState(false) | ||
| const [escenas, setEscenas] = useState<string[]>([]) | ||
| const [seleccionada, setSeleccionada] = useState<string>('') | ||
| const estado = useRef<EstadoDePrueba>({ ready: false, ack: null, error: null, enviada: null }) | ||
|
|
||
| const anotar = useCallback((texto: string, entrante = false) => { | ||
| setLog((prev) => | ||
| [{ hora: new Date().toISOString().slice(11, 23), texto, entrante }, ...prev].slice(0, 60), | ||
| ) | ||
| }, []) | ||
|
|
||
| const publicarEstado = useCallback(() => { | ||
| window.__holtmontTest = { ...estado.current } | ||
| }, []) | ||
|
|
||
| // Catálogo de escenas disponibles y la que pide la URL (`?scene=`). | ||
| useEffect(() => { | ||
| publicarEstado() | ||
| const pedida = new URLSearchParams(window.location.search).get('scene') | ||
| fetch('/holtmont-fixtures/index.json') | ||
| .then((r) => r.json()) | ||
| .then((lista: string[]) => { | ||
| setEscenas(lista) | ||
| setSeleccionada(pedida && lista.includes(pedida) ? pedida : (lista[0] ?? '')) | ||
| }) | ||
| .catch((err) => anotar(`no se pudo leer el índice de escenas: ${err}`)) | ||
| }, [anotar, publicarEstado]) | ||
|
|
||
| const enviarEscena = useCallback( | ||
| async (nombre: string) => { | ||
| const iframe = iframeRef.current | ||
| if (!iframe?.contentWindow || !nombre) return | ||
| estado.current = { ...estado.current, ack: null, error: null, enviada: null } | ||
| publicarEstado() | ||
| anotar(`→ HOLTMONT_3D_IMPORT (${nombre})`) | ||
| const projectData = await fetch(`/holtmont-fixtures/${nombre}.json`).then((r) => r.json()) | ||
| iframe.contentWindow.postMessage({ type: 'HOLTMONT_3D_IMPORT', projectData }, '*') | ||
| estado.current = { ...estado.current, enviada: nombre } | ||
| publicarEstado() | ||
| }, | ||
| [anotar, publicarEstado], | ||
| ) | ||
|
|
||
| useEffect(() => { | ||
| const handler = (event: MessageEvent) => { | ||
| const data = event.data as Record<string, unknown> | null | ||
| if (!data?.type) return | ||
|
|
||
| if (data.type === 'PASCAL_READY') { | ||
| anotar('← PASCAL_READY', true) | ||
| setReady(true) | ||
| estado.current = { ...estado.current, ready: true } | ||
| } else if (data.type === 'HOLTMONT_3D_IMPORT_ACK') { | ||
| const descartes = (data.dropped as unknown[]) ?? [] | ||
| anotar( | ||
| `← HOLTMONT_3D_IMPORT_ACK (${data.nodeCount} nodos, ${descartes.length} descartes)`, | ||
| true, | ||
| ) | ||
| estado.current = { ...estado.current, ack: data } | ||
| } else if (data.type === 'HOLTMONT_3D_IMPORT_ERROR') { | ||
| anotar(`← HOLTMONT_3D_IMPORT_ERROR: ${data.reason}`, true) | ||
| estado.current = { ...estado.current, error: data } | ||
| } else if (data.type === 'HOLTMONT_3D_EXPORT') { | ||
| const exportData = data.data as Record<string, unknown> | ||
| anotar(`← HOLTMONT_3D_EXPORT (${Object.keys(exportData?.nodes ?? {}).length} nodos)`, true) | ||
| } else { | ||
| return | ||
| } | ||
| publicarEstado() | ||
| } | ||
|
|
||
| window.addEventListener('message', handler) | ||
| return () => window.removeEventListener('message', handler) | ||
| }, [anotar, publicarEstado]) | ||
|
|
||
| // Autoenvío en cuanto el editor avisa que está listo: así la prueba de humo | ||
| // no depende de un temporizador. | ||
| useEffect(() => { | ||
| if (ready && seleccionada && !estado.current.enviada) { | ||
| void enviarEscena(seleccionada) | ||
| } | ||
| }, [ready, seleccionada, enviarEscena]) | ||
|
|
||
| return ( | ||
| <div style={{ display: 'flex', height: '100vh', fontFamily: 'monospace' }}> | ||
| <div | ||
| style={{ | ||
| width: 320, | ||
| padding: 16, | ||
| borderRight: '1px solid #333', | ||
| background: '#111', | ||
| color: '#eee', | ||
| overflowY: 'auto', | ||
| }} | ||
| > | ||
| <h2 style={{ margin: '0 0 12px', fontSize: 14 }}>Puente Holtmont — banco de pruebas</h2> | ||
|
|
||
| <div style={{ fontSize: 11, color: ready ? '#4ade80' : '#facc15', marginBottom: 8 }}> | ||
| Editor: {ready ? '✓ listo' : 'esperando…'} | ||
| </div> | ||
|
|
||
| <select | ||
| onChange={(e) => setSeleccionada(e.target.value)} | ||
| style={{ width: '100%', marginBottom: 6, padding: 4, fontSize: 12 }} | ||
| value={seleccionada} | ||
| > | ||
| {escenas.map((nombre) => ( | ||
| <option key={nombre} value={nombre}> | ||
| {nombre} | ||
| </option> | ||
| ))} | ||
| </select> | ||
|
|
||
| <button | ||
| data-testid="enviar-escena" | ||
| disabled={!(ready && seleccionada)} | ||
| onClick={() => void enviarEscena(seleccionada)} | ||
| style={{ | ||
| width: '100%', | ||
| padding: '6px 0', | ||
| cursor: ready ? 'pointer' : 'not-allowed', | ||
| background: ready ? '#16a34a' : '#374151', | ||
| color: '#fff', | ||
| border: 'none', | ||
| borderRadius: 4, | ||
| fontSize: 12, | ||
| marginBottom: 12, | ||
| }} | ||
| > | ||
| Enviar escena (IMPORT) | ||
| </button> | ||
|
|
||
| <div style={{ fontSize: 10, color: '#9ca3af', marginBottom: 4 }}>Bitácora:</div> | ||
| <div style={{ fontSize: 10, lineHeight: 1.6 }}> | ||
| {log.map((entrada) => ( | ||
| <div | ||
| key={`${entrada.hora}-${entrada.texto}`} | ||
| style={{ color: entrada.entrante ? '#86efac' : '#93c5fd' }} | ||
| > | ||
| {entrada.hora} {entrada.texto} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
|
|
||
| <iframe ref={iframeRef} src="/" style={{ flex: 1, border: 'none' }} title="Pascal Editor" /> | ||
| </div> | ||
| ) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import leaves editor selection stale
High Severity
HoltmontBridgeapplies an imported graph withsetSceneonly and never runsapplySceneGraphToEditororsyncEditorSelectionFromCurrentScene. ViewerbuildingIdandlevelIdkeep pointing at the previous scene, so the floorplan resolves no walls, slabs, or ceilings and the imported project looks empty after a successful ACK.Reviewed by Cursor Bugbot for commit 21db2b0. Configure here.