diff --git a/apps/editor/components/ui/item-catalog/item-catalog.tsx b/apps/editor/components/ui/item-catalog/item-catalog.tsx index 0380e6c2..e6c00856 100644 --- a/apps/editor/components/ui/item-catalog/item-catalog.tsx +++ b/apps/editor/components/ui/item-catalog/item-catalog.tsx @@ -12,6 +12,7 @@ import { cn } from "@/lib/utils"; import useEditor, { CatalogCategory } from "@/store/use-editor"; import { CATALOG_ITEMS } from "./catalog-items"; import { AssetInput } from "@pascal-app/core"; +import { resolveCdnUrl } from "@pascal-app/viewer"; export function ItemCatalog({ category }: { category: CatalogCategory }) { const selectedItem = useEditor((state) => state.selectedItem); @@ -62,7 +63,7 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) { alt={item.name} className="rounded-lg object-cover" fill - src={item.thumbnail} + src={resolveCdnUrl(item.thumbnail) || ''} /> {attachmentIcon && (
diff --git a/bun.lock b/bun.lock index 51241704..d9c9ff13 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "editor", @@ -63,14 +62,16 @@ }, "packages/core": { "name": "@pascal-app/core", - "version": "0.0.1", + "version": "0.1.3", "dependencies": { "dedent": "^1.7.1", "idb-keyval": "^6.2.2", "mitt": "^3.0.1", "nanoid": "^5.1.6", + "three-bvh-csg": "^0.0.17", "zod": "^4.3.5", "zundo": "^2.3.0", + "zustand": "^5", }, "devDependencies": { "@repo/typescript-config": "*", @@ -81,6 +82,7 @@ "peerDependencies": { "@react-three/drei": "^10", "@react-three/fiber": "^9", + "react": "^18 || ^19", "three": "^0.182", }, }, @@ -124,7 +126,10 @@ }, "packages/viewer": { "name": "@pascal-app/viewer", - "version": "0.0.1", + "version": "0.1.3", + "dependencies": { + "zustand": "^5", + }, "devDependencies": { "@repo/typescript-config": "*", "@types/react": "^19.2.2", @@ -132,9 +137,10 @@ "typescript": "5.9.2", }, "peerDependencies": { - "@pascal-app/core": "*", + "@pascal-app/core": "^0.1.3", "@react-three/drei": "^10", "@react-three/fiber": "^9", + "react": "^18 || ^19", "three": "^0.182", }, }, diff --git a/packages/core/package.json b/packages/core/package.json index 4a2b8a37..0436be18 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,33 +1,70 @@ { "name": "@pascal-app/core", - "version": "0.0.1", - "private": true, + "version": "0.1.5", + "description": "Core library for Pascal 3D building editor", "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": { "types": "./src/index.ts", + "import": "./src/index.ts", "default": "./src/index.ts" } }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc --declaration --emitDeclarationOnly && tsc", + "prepublishOnly": "npm run build" + }, + "publishConfig": { + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + } + }, "peerDependencies": { "@react-three/drei": "^10", "@react-three/fiber": "^9", + "react": "^18 || ^19", "three": "^0.182" }, + "dependencies": { + "dedent": "^1.7.1", + "idb-keyval": "^6.2.2", + "mitt": "^3.0.1", + "nanoid": "^5.1.6", + "three-bvh-csg": "^0.0.17", + "zod": "^4.3.5", + "zundo": "^2.3.0", + "zustand": "^5" + }, "devDependencies": { "@repo/typescript-config": "*", "@types/react": "^19.2.2", "typescript": "5.9.2", "@types/three": "^0.182.0" }, - "dependencies": { - "dedent": "^1.7.1", - "idb-keyval": "^6.2.2", - "mitt": "^3.0.1", - "nanoid": "^5.1.6", - "zod": "^4.3.5", - "zundo": "^2.3.0" - } + "keywords": [ + "3d", + "building", + "editor", + "architecture", + "webgpu", + "three.js" + ], + "repository": { + "type": "git", + "url": "https://github.com/your-username/pascal-editor.git", + "directory": "packages/core" + }, + "license": "MIT" } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index d2c35871..a47d5777 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -4,6 +4,6 @@ "outDir": "dist", "rootDir": "src" }, - "include": ["src", "../viewer/src/systems/level"], + "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/packages/viewer/package.json b/packages/viewer/package.json index f4594b34..2ae3e6de 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -1,26 +1,66 @@ { "name": "@pascal-app/viewer", - "version": "0.0.1", - "private": true, + "version": "0.1.5", + "description": "3D viewer component for Pascal building editor", "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": { "types": "./src/index.ts", + "import": "./src/index.ts", "default": "./src/index.ts" } }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc --declaration --emitDeclarationOnly && tsc", + "prepublishOnly": "npm run build" + }, + "publishConfig": { + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + } + }, "peerDependencies": { - "@pascal-app/core": "*", + "@pascal-app/core": "^0.1.4", "@react-three/drei": "^10", "@react-three/fiber": "^9", + "react": "^18 || ^19", "three": "^0.182" }, + "dependencies": { + "zustand": "^5" + }, "devDependencies": { "@repo/typescript-config": "*", "@types/react": "^19.2.2", "typescript": "5.9.2", "@types/three": "^0.182.0" - } + }, + "keywords": [ + "3d", + "building", + "editor", + "viewer", + "architecture", + "webgpu", + "three.js", + "react-three-fiber" + ], + "repository": { + "type": "git", + "url": "https://github.com/your-username/pascal-editor.git", + "directory": "packages/viewer" + }, + "license": "MIT" } diff --git a/packages/viewer/src/components/renderers/item/item-renderer.tsx b/packages/viewer/src/components/renderers/item/item-renderer.tsx index f247dd29..34bc1d9f 100644 --- a/packages/viewer/src/components/renderers/item/item-renderer.tsx +++ b/packages/viewer/src/components/renderers/item/item-renderer.tsx @@ -4,6 +4,7 @@ import { useGLTF } from '@react-three/drei/core/Gltf' import { Suspense, useEffect, useMemo, useRef } from 'react' import type { Group, Material, Mesh } from 'three' import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu' +import { resolveCdnUrl } from '../../../lib/asset-url' import { useNodeEvents } from '../../../hooks/use-node-events' // Shared materials to avoid creating new instances for every mesh @@ -46,7 +47,7 @@ export const ItemRenderer = ({ node }: { node: ItemNode }) => { } const ModelRenderer = ({ node }: { node: ItemNode }) => { - const { scene, nodes } = useGLTF(node.asset.src) + const { scene, nodes } = useGLTF(resolveCdnUrl(node.asset.src) || '') if (nodes.cutout) { nodes.cutout.visible = false @@ -64,19 +65,19 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => { if ((child as Mesh).isMesh) { const mesh = child as Mesh if (mesh.name === 'cutout') { - child.visible = false; + child.visible = false return } - let hasGlass = false; + let hasGlass = false // Handle both single material and material array cases if (Array.isArray(mesh.material)) { mesh.material = mesh.material.map((mat) => getMaterialForOriginal(mat)) - hasGlass = mesh.material.some(mat => mat.name === 'glass'); + hasGlass = mesh.material.some((mat) => mat.name === 'glass') } else { mesh.material = getMaterialForOriginal(mesh.material) - hasGlass = mesh.material.name === 'glass'; + hasGlass = mesh.material.name === 'glass' } mesh.castShadow = !hasGlass mesh.receiveShadow = !hasGlass diff --git a/packages/viewer/src/components/renderers/scan/scan-renderer.tsx b/packages/viewer/src/components/renderers/scan/scan-renderer.tsx index e07684a4..67e593a7 100644 --- a/packages/viewer/src/components/renderers/scan/scan-renderer.tsx +++ b/packages/viewer/src/components/renderers/scan/scan-renderer.tsx @@ -27,7 +27,8 @@ export const ScanRenderer = ({ node }: { node: ScanNode }) => { } const ScanModel = ({ url, opacity }: { url: string; opacity: number }) => { - const { scene } = useGLTFKTX2(url) + const gltf = useGLTFKTX2(url) as any + const scene = gltf.scene useMemo(() => { const normalizedOpacity = opacity / 100 @@ -44,7 +45,7 @@ const ScanModel = ({ url, opacity }: { url: string; opacity: number }) => { material.needsUpdate = true } - scene.traverse((child) => { + scene.traverse((child: any) => { if ((child as Mesh).isMesh) { const mesh = child as Mesh diff --git a/packages/viewer/src/hooks/use-gltf-ktx2.tsx b/packages/viewer/src/hooks/use-gltf-ktx2.tsx index 7b4bc4de..70d93cd3 100644 --- a/packages/viewer/src/hooks/use-gltf-ktx2.tsx +++ b/packages/viewer/src/hooks/use-gltf-ktx2.tsx @@ -6,7 +6,7 @@ import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.j const ktx2LoaderInstance = new KTX2Loader() ktx2LoaderInstance.setTranscoderPath('https://cdn.jsdelivr.net/gh/pmndrs/drei-assets@master/basis/') -const useGLTFKTX2 = (path: string) => { +const useGLTFKTX2 = (path: string): ReturnType => { const gl = useThree((state) => state.gl) return useGLTF(path, true, true, (loader) => { diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts index 31d4fd9e..21aa3ebd 100644 --- a/packages/viewer/src/index.ts +++ b/packages/viewer/src/index.ts @@ -1,3 +1,4 @@ export { default as Viewer } from './components/viewer' export { useGridEvents } from './hooks/use-grid-events' export { default as useViewer } from './store/use-viewer' +export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url' \ No newline at end of file diff --git a/packages/viewer/src/lib/asset-url.ts b/packages/viewer/src/lib/asset-url.ts new file mode 100644 index 00000000..8c1583e2 --- /dev/null +++ b/packages/viewer/src/lib/asset-url.ts @@ -0,0 +1,51 @@ +import { loadAssetUrl } from '@pascal-app/core' + +export const ASSETS_CDN_URL = 'https://pascal-cdn.wawasensei.dev' + +/** + * Resolves an asset URL to the appropriate format: + * - If URL starts with http:// or https://, return as-is (external URL) + * - If URL starts with asset://, resolve from IndexedDB storage + * - If URL starts with /, prepend CDN URL (absolute path) + * - Otherwise, prepend CDN URL (relative path) + */ +export async function resolveAssetUrl(url: string | undefined | null): Promise { + if (!url) return null + + // External URL - use as-is + if (url.startsWith('http://') || url.startsWith('https://')) { + return url + } + + // IndexedDB asset - resolve from storage + if (url.startsWith('asset://')) { + return loadAssetUrl(url) + } + + // Absolute or relative path - prepend CDN URL + const normalizedPath = url.startsWith('/') ? url : `/${url}` + return `${ASSETS_CDN_URL}${normalizedPath}` +} + +/** + * Synchronous version for URLs that don't need IndexedDB resolution + * Only use this if you're sure the URL is not an asset:// URL + */ +export function resolveCdnUrl(url: string | undefined | null): string | null { + if (!url) return null + + // External URL - use as-is + if (url.startsWith('http://') || url.startsWith('https://')) { + return url + } + + // Don't use this for asset:// URLs - use resolveAssetUrl instead + if (url.startsWith('asset://')) { + console.warn('Use resolveAssetUrl() for asset:// URLs, not resolveCdnUrl()') + return null + } + + // Absolute or relative path - prepend CDN URL + const normalizedPath = url.startsWith('/') ? url : `/${url}` + return `${ASSETS_CDN_URL}${normalizedPath}` +} diff --git a/packages/viewer/src/store/use-viewer.d.ts b/packages/viewer/src/store/use-viewer.d.ts new file mode 100644 index 00000000..736f9544 --- /dev/null +++ b/packages/viewer/src/store/use-viewer.d.ts @@ -0,0 +1,35 @@ +import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from "@pascal-app/core"; +import type { Object3D } from "three"; +type SelectionPath = { + buildingId: BuildingNode["id"] | null; + levelId: LevelNode["id"] | null; + zoneId: ZoneNode["id"] | null; + selectedIds: BaseNode["id"][]; +}; +type Outliner = { + selectedObjects: Object3D[]; + hoveredObjects: Object3D[]; +}; +type ViewerState = { + selection: SelectionPath; + hoveredId: AnyNode['id'] | ZoneNode['id'] | null; + setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void; + cameraMode: 'perspective' | 'orthographic'; + setCameraMode: (mode: 'perspective' | 'orthographic') => void; + levelMode: 'stacked' | 'exploded' | 'solo' | 'manual'; + setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void; + wallMode: 'up' | 'cutaway' | 'down'; + setWallMode: (mode: 'up' | 'cutaway' | 'down') => void; + showScans: boolean; + setShowScans: (show: boolean) => void; + showGuides: boolean; + setShowGuides: (show: boolean) => void; + setSelection: (updates: Partial) => void; + resetSelection: () => void; + outliner: Outliner; + exportScene: (() => Promise) | null; + setExportScene: (fn: (() => Promise) | null) => void; +}; +declare const useViewer: import("zustand").UseBoundStore>; +export default useViewer; +//# sourceMappingURL=use-viewer.d.ts.map \ No newline at end of file diff --git a/packages/viewer/src/store/use-viewer.d.ts.map b/packages/viewer/src/store/use-viewer.d.ts.map new file mode 100644 index 00000000..7a03414b --- /dev/null +++ b/packages/viewer/src/store/use-viewer.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"use-viewer.d.ts","sourceRoot":"","sources":["use-viewer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAItC,KAAK,aAAa,GAAG;IACnB,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACtC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;CAC/B,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,cAAc,EAAE,QAAQ,EAAE,CAAC;CAC5B,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,aAAa,CAAA;IACxB,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChD,YAAY,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;IAEjE,UAAU,EAAE,aAAa,GAAG,cAAc,CAAA;IAC1C,aAAa,EAAE,CAAC,IAAI,EAAE,aAAa,GAAG,cAAc,KAAK,IAAI,CAAA;IAE7D,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;IACrD,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAA;IAExE,QAAQ,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,CAAA;IACnC,WAAW,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,KAAK,IAAI,CAAA;IAEtD,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAErC,UAAU,EAAE,OAAO,CAAA;IACnB,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAGtC,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,KAAK,IAAI,CAAA;IACvD,cAAc,EAAE,MAAM,IAAI,CAAA;IAE1B,QAAQ,EAAE,QAAQ,CAAA;IAGlB,WAAW,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;IACzC,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,KAAK,IAAI,CAAA;CAC3D,CAAA;AAED,QAAA,MAAM,SAAS,0EAqDZ,CAAC;AAEJ,eAAe,SAAS,CAAC"} \ No newline at end of file diff --git a/packages/viewer/src/systems/level/level-system.d.ts b/packages/viewer/src/systems/level/level-system.d.ts new file mode 100644 index 00000000..5f5df3f9 --- /dev/null +++ b/packages/viewer/src/systems/level/level-system.d.ts @@ -0,0 +1,2 @@ +export declare const LevelSystem: () => null; +//# sourceMappingURL=level-system.d.ts.map \ No newline at end of file diff --git a/packages/viewer/src/systems/level/level-system.d.ts.map b/packages/viewer/src/systems/level/level-system.d.ts.map new file mode 100644 index 00000000..e98f88d6 --- /dev/null +++ b/packages/viewer/src/systems/level/level-system.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"level-system.d.ts","sourceRoot":"","sources":["level-system.tsx"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,YAkBvB,CAAA"} \ No newline at end of file