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