From 09137bc2e097cd756538b8eb533aa60afc79b677 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sat, 26 Sep 2026 20:58:24 -0400 Subject: [PATCH 1/8] test: peer ranges accept the shipped host (core 1.x, three 0.186), failing first Co-Authored-By: Claude Opus 5.5 (1M context) --- src/package.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/package.test.ts diff --git a/src/package.test.ts b/src/package.test.ts new file mode 100644 index 0000000..62aec6c --- /dev/null +++ b/src/package.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from 'bun:test' +import { readFileSync } from 'node:fs' + +const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { + peerDependencies: Record +} + +// The host ships Pascal 1.x on three 0.186 (@pascal-app/core 1.0.3 pins `three ^0.186`); +// strict installs refuse the plugin when a peer range excludes them. +const shippedHost = { + '@pascal-app/core': ['1.0.0-beta.4', '1.0.3', '1.9.0'], + '@pascal-app/editor': ['1.0.0-beta.4', '1.0.3', '1.9.0'], + '@pascal-app/viewer': ['1.0.0-beta.4', '1.0.3', '1.9.0'], + three: ['0.185.0', '0.186.0', '0.186.1'], +} as const + +describe('peer ranges', () => { + for (const [name, versions] of Object.entries(shippedHost)) { + test(`${name} accepts ${versions.join(', ')}`, () => { + const range = manifest.peerDependencies[name] + expect(range).toBeDefined() + for (const version of versions) { + expect({ version, accepted: Bun.semver.satisfies(version, range ?? '') }).toEqual({ + version, + accepted: true, + }) + } + }) + } + + test('does not claim the next major', () => { + for (const name of ['@pascal-app/core', '@pascal-app/editor', '@pascal-app/viewer']) { + expect(Bun.semver.satisfies('2.0.0', manifest.peerDependencies[name] ?? '')).toBe(false) + } + expect(Bun.semver.satisfies('0.187.0', manifest.peerDependencies.three ?? '')).toBe(false) + }) +}) From b14253a508875a0a79f890c91d4995c935dca728 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sat, 26 Sep 2026 20:59:34 -0400 Subject: [PATCH 2/8] fix: accept Pascal 1.x and three 0.186 as peers The host ships @pascal-app/core 1.0.3 on three 0.186, which the <1.0.0 and ^0.185 ranges excluded, so strict installs refused the plugin. Dev dependencies follow the host (core/editor/viewer 1.0.3, three 0.186.0, @types/three 0.184.1 as in pascalorg/editor). Co-Authored-By: Claude Opus 5.5 (1M context) --- bun.lock | 94 ++++++++++++++++++++++++++++++++++++++++++++-------- package.json | 18 +++++----- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/bun.lock b/bun.lock index 013b55e..8e189be 100644 --- a/bun.lock +++ b/bun.lock @@ -8,9 +8,9 @@ "urdf-loader": "^0.13.1", }, "devDependencies": { - "@pascal-app/core": "1.0.0-beta.4", - "@pascal-app/editor": "1.0.0-beta.4", - "@pascal-app/viewer": "1.0.0-beta.4", + "@pascal-app/core": "1.0.3", + "@pascal-app/editor": "1.0.3", + "@pascal-app/viewer": "1.0.3", "@react-three/fiber": "^9.5.0", "@types/blob-stream": "^0.1.33", "@types/bun": "^1.3.5", @@ -19,21 +19,21 @@ "@types/pdfkit": "^0.17.3", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", - "@types/three": "^0.185.0", + "@types/three": "0.184.1", "react": "^19.2.4", "react-dom": "^19.2.4", - "three": "^0.185.0", + "three": "0.186.0", "typescript": "6.0.3", "zod": "^4.3.6", "zustand": "^5.0.11", }, "peerDependencies": { - "@pascal-app/core": ">=1.0.0-beta.4 <1.0.0", - "@pascal-app/editor": ">=1.0.0-beta.4 <1.0.0", - "@pascal-app/viewer": ">=1.0.0-beta.4 <1.0.0", + "@pascal-app/core": ">=1.0.0-beta.4 <2.0.0", + "@pascal-app/editor": ">=1.0.0-beta.4 <2.0.0", + "@pascal-app/viewer": ">=1.0.0-beta.4 <2.0.0", "@react-three/fiber": "^9", "react": "^18 || ^19", - "three": "^0.185", + "three": "^0.185 || ^0.186", "zod": "^4", "zustand": "^5", }, @@ -62,6 +62,12 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], + "@gltf-transform/core": ["@gltf-transform/core@4.5.0", "", { "dependencies": { "property-graph": "^4.1.0" } }, "sha512-JlvShvnB4AtfGZqjnESS/wfPJbpVnyHhTwJBbWN4qXqkB3C5HIoxxpu/orwGBge6Pc0zObeWj70lVf+4gbuu0A=="], + + "@gltf-transform/extensions": ["@gltf-transform/extensions@4.5.0", "", { "dependencies": { "@gltf-transform/core": "^4.5.0", "ktx-parse": "^1.1.0" } }, "sha512-J4cKzrmEr75YkrtdI4cKosBg2vDC4rT56aq/QoijW7GT1Y9gSEMJbqyFO9xcFO3FNpZaZiDrHWUqE5Okrl5nOQ=="], + + "@gltf-transform/functions": ["@gltf-transform/functions@4.5.0", "", { "dependencies": { "@gltf-transform/core": "^4.5.0", "@gltf-transform/extensions": "^4.5.0", "ktx-parse": "^1.1.0", "ndarray": "^1.0.19", "ndarray-lanczos": "^0.3.0", "ndarray-pixels": "^5.2.0" } }, "sha512-7sMOuEfksMyF+/NTwUnEOgh7djohJ5XfeYg8rzXDG+9JIdbGVUpvJUY3XVBJCUJJ04dusDvcTHloOi1wfM8lnw=="], + "@iconify/react": ["@iconify/react@6.0.2", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-SMmC2sactfpJD427WJEDN6PMyznTFMhByK9yLW0gOTtnjzzbsi/Ke/XqsumsavFPwNiXs8jSiYeZTmLCLwO+Fg=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], @@ -120,6 +126,14 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jscadui/3mf-export": ["@jscadui/3mf-export@0.5.0", "", {}, "sha512-y5vZktqCjyi7wA38zqNlLIdZUIRZoOO9vCjLzwmL4bR0hk7B/Zm1IeffzJPFe1vFc0C1IEv3hm8caDv3doRk9g=="], + "@mediapipe/tasks-vision": ["@mediapipe/tasks-vision@0.10.17", "", {}, "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="], "@monogrid/gainmap-js": ["@monogrid/gainmap-js@3.4.0", "", { "dependencies": { "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg=="], @@ -146,15 +160,17 @@ "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@nodable/entities": ["@nodable/entities@3.0.0", "", {}, "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw=="], + "@number-flow/react": ["@number-flow/react@0.6.2", "", { "dependencies": { "esm-env": "^1.1.4", "number-flow": "0.6.2" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-WjZuV4aA+vhRCgCF+adGLgFVVAJ8vdvq6EchRT2FiBIgElTXtDOA3YgkcMr9UHpvpS9v4H70AB5wZv0D9jc3QA=="], - "@pascal-app/core": ["@pascal-app/core@1.0.0-beta.4", "", { "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", "zustand": "^5" }, "peerDependencies": { "@react-three/drei": "^10", "@react-three/fiber": "^9", "react": "^18 || ^19", "three": "^0.185" } }, "sha512-g07X0t28crhbYrTQcrWImLnRuB/zDLLGAnAt16T8zyp5UBxMMmeh3kPqSqqZQxkDGx261PUG31Qf06TlKcniuw=="], + "@pascal-app/core": ["@pascal-app/core@1.0.3", "", { "dependencies": { "dedent": "^1.7.1", "idb-keyval": "^6.2.2", "mitt": "^3.0.1", "nanoid": "^5.1.6", "zod": ">=4.5.4 <4.6", "zundo": "^2.3.0", "zustand": "^5" }, "peerDependencies": { "@react-three/drei": "^10", "@react-three/fiber": "^9", "react": "^18 || ^19", "three": "^0.186" } }, "sha512-v95AfNJZDG/N7Biwt/ZQCtZuulRtqNuwAiT3cb1/F63hqZmqmoZnTLZGCu+w0Kxu57UFK084YrbtUP+cbGo1sQ=="], - "@pascal-app/editor": ["@pascal-app/editor@1.0.0-beta.4", "", { "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@iconify/react": "^6.0.2", "@number-flow/react": "^0.6.0", "@pascal-app/lingo": "^0.2.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@visual-json/react": "^0.4.0", "blob-stream": "^0.1.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "howler": "^2.2.4", "lucide-react": "^1.7.0", "mitt": "^3.0.1", "motion": "^12.34.3", "nanoid": "^5.1.6", "pdfkit": "^0.19.1", "tailwind-merge": "^3.5.0", "three-mesh-bvh": "~0.9.8", "zod": "^4.3.6", "zustand": "^5.0.11" }, "peerDependencies": { "@pascal-app/core": "^1.0.0-beta.4", "@pascal-app/viewer": "^1.0.0-beta.4", "@react-three/drei": "^10", "@react-three/fiber": "^9", "next": ">=15", "react": "^18 || ^19", "react-dom": "^18 || ^19", "three": "^0.185" } }, "sha512-2ED/NkN+IfimU7rA3ISv7EeVF/MjYOEaXtoIjSLSIrQyf61jWt7Mdt0pdpxR0dm9VGnRa03MqPCG0DJgw2mF6A=="], + "@pascal-app/editor": ["@pascal-app/editor@1.0.3", "", { "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@iconify/react": "^6.0.2", "@number-flow/react": "^0.6.0", "@pascal-app/lingo": "^0.2.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@visual-json/react": "^0.4.0", "blob-stream": "^0.1.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "fflate": "^0.8.3", "howler": "^2.2.4", "lucide-react": "^1.7.0", "manifold-3d": "3.5.1", "mitt": "^3.0.1", "motion": "^12.34.3", "nanoid": "^5.1.6", "pdfkit": "^0.19.1", "tailwind-merge": "^3.5.0", "three-bvh-csg": "^0.0.18", "three-mesh-bvh": "~0.9.8", "zod": ">=4.5.4 <4.6", "zustand": "^5.0.11" }, "peerDependencies": { "@pascal-app/core": "^1.0.3", "@pascal-app/viewer": "^1.0.3", "@react-three/drei": "^10", "@react-three/fiber": "^9", "next": ">=15", "react": "^18 || ^19", "react-dom": "^18 || ^19", "three": "^0.186" } }, "sha512-+LSJ+mxj3JsTM18aay3qexn6OKGinN38ZBko3vNdf79sge/iFWPOXquniiMpkEVtMz4lRHlLM2+fc6WvXT5RHA=="], "@pascal-app/lingo": ["@pascal-app/lingo@0.2.1", "", { "peerDependencies": { "react": "^19" }, "optionalPeers": ["react"] }, "sha512-Y6Nmb4vLSMp/dIdKABc7GjdTdB6yvUyBg95ySlThMz6zKrJjpVG0utexY4cnmWVPkGNSKOFotiHw8UJiQMUVnw=="], - "@pascal-app/viewer": ["@pascal-app/viewer@1.0.0-beta.4", "", { "dependencies": { "three-bvh-csg": "^0.0.18", "three-mesh-bvh": "^0.9.8", "zustand": "^5" }, "peerDependencies": { "@pascal-app/core": "^1.0.0-beta.4", "@react-three/drei": "^10", "@react-three/fiber": "^9", "react": "^18 || ^19", "three": "^0.185" } }, "sha512-R6TGwIiOhzXNUIWjnG/Qy9GxGS7XQvWOOxgWFekjl3cdejl02M4Z98v90gVw7IDQGmZn/g5CCVJhRaKKtSHBBQ=="], + "@pascal-app/viewer": ["@pascal-app/viewer@1.0.3", "", { "dependencies": { "three-bvh-csg": "^0.0.18", "three-mesh-bvh": "^0.9.8", "zustand": "^5" }, "peerDependencies": { "@pascal-app/core": "^1.0.3", "@react-three/drei": "^10", "@react-three/fiber": "^9", "react": "^18 || ^19", "react-dom": "^18 || ^19", "three": "^0.186" } }, "sha512-HCoKEPVDhDJOl+ydivkwnwpE2If+EK6BgSwMsF/SZqZQJ9jkpt5ZTL1osVutr6XQP916Hf/pbHML8qhWqYjBkw=="], "@radix-ui/number": ["@radix-ui/number@1.1.3", "", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="], @@ -250,6 +266,8 @@ "@types/howler": ["@types/howler@2.2.13", "", {}, "sha512-40+EBjqIHHrC4VShlz/7i0lBUsE3QkgzZinQQji74Hd8sBkJZUBaT7LWFLK6rcabsDOOQpoMbEJvtaFQwxOu/g=="], + "@types/ndarray": ["@types/ndarray@1.1.0", "", {}, "sha512-go+C6vHBUuO9jH6Tj6qEZ63JEGoMSxFdmd+6QAtthGD6xumBfIz3RbNDDs2aoRVLf9naH8lk5jnWy2UHHKgYQQ=="], + "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], @@ -264,7 +282,7 @@ "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], - "@types/three": ["@types/three@0.185.4", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.2", "meshoptimizer": "~1.1.1" } }, "sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA=="], + "@types/three": ["@types/three@0.184.1", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.2", "meshoptimizer": "~1.1.1" } }, "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA=="], "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], @@ -276,6 +294,8 @@ "@visual-json/react": ["@visual-json/react@0.4.0", "", { "dependencies": { "@visual-json/core": "0.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-qiDrXI7wu9VoTE0TaLoJp7kwRj8SQTpkS/wkts5sBPA8078zdrIKc+0u0Y8wAXGYd3udw9dNfD4kmpO48OEwXQ=="], + "anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -310,12 +330,18 @@ "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "cwise-compiler": ["cwise-compiler@1.1.3", "", { "dependencies": { "uniq": "^1.0.0" } }, "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ=="], + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], "detect-gpu": ["detect-gpu@5.0.70", "", { "dependencies": { "webgl-constants": "^1.1.1" } }, "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w=="], @@ -328,10 +354,16 @@ "draco3d": ["draco3d@1.5.7", "", {}, "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="], + "esbuild-wasm": ["esbuild-wasm@0.27.7", "", { "bin": { "esbuild": "bin/esbuild" } }, "sha512-1k03e2/tGz+sLz3/xzoZmUsIqtaGIvJa8k4UqUeqCUry83nHmlxQYZUUES0WBFUYilSQUf7nDUGAciIIklljSg=="], + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-xml-builder": ["fast-xml-builder@1.3.1", "", { "dependencies": { "path-expression-matcher": "^1.6.2", "xml-naming": "^0.3.0" } }, "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug=="], + + "fast-xml-parser": ["fast-xml-parser@5.11.1", "", { "dependencies": { "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", "is-unsafe": "^2.0.0", "path-expression-matcher": "^1.6.2", "strnum": "^2.4.2", "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], @@ -352,14 +384,22 @@ "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + "iota-array": ["iota-array@1.0.0", "", {}, "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA=="], + + "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + "is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], + "is-unsafe": ["is-unsafe@2.0.2", "", {}, "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "its-fine": ["its-fine@2.0.0", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng=="], "js-md5": ["js-md5@0.8.3", "", {}, "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ=="], + "ktx-parse": ["ktx-parse@1.1.0", "", {}, "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ=="], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "linebreak": ["linebreak@1.1.0", "", { "dependencies": { "base64-js": "0.0.8", "unicode-trie": "^2.0.0" } }, "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ=="], @@ -368,6 +408,10 @@ "maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "manifold-3d": ["manifold-3d@3.5.1", "", { "dependencies": { "@gltf-transform/core": "^4.2.0", "@gltf-transform/extensions": "^4.2.0", "@gltf-transform/functions": "^4.2.0", "@jridgewell/resolve-uri": "^3.1.2", "@jridgewell/trace-mapping": "^0.3.31", "@jscadui/3mf-export": "^0.5.0", "commander": "^13.1.0", "convert-source-map": "^2.0.0", "fast-xml-parser": "^5.4.2", "fflate": "^0.8.0", "magic-string": "^0.30.21" }, "peerDependencies": { "esbuild-wasm": "^0.27.3" }, "bin": { "manifold-cad": "bin/manifold-cad" } }, "sha512-/+m6kxYMMhnPutcQ5oSmFJiJ+gyP/0fmuUCb9Qeaunvecm/bfqogKYDDJarsnWiFioSMtKheF+lGmSlnYCik9g=="], + "meshline": ["meshline@3.3.1", "", { "peerDependencies": { "three": ">=0.137" } }, "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ=="], "meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], @@ -382,12 +426,22 @@ "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], + "ndarray": ["ndarray@1.1.1", "", { "dependencies": { "iota-array": "^1.0.0", "is-buffer": "^1.0.2" } }, "sha512-AsE8w/M2MS+0pD3nP5qbRRtJFeJJJaJ+qXZaZr0XjVGXhy6LIj2TERTlkpdK2AI3WufoOP+pwPk3q2ilAJcZ9g=="], + + "ndarray-lanczos": ["ndarray-lanczos@0.3.0", "", { "dependencies": { "@types/ndarray": "^1.0.11", "ndarray": "^1.0.19" } }, "sha512-5kBmmG3Zvyj77qxIAC4QFLKuYdDIBJwCG+DukT6jQHNa1Ft74/hPH1z5mbQXeHBt8yvGPBGVrr3wEOdJPYYZYg=="], + + "ndarray-ops": ["ndarray-ops@1.2.2", "", { "dependencies": { "cwise-compiler": "^1.0.0" } }, "sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw=="], + + "ndarray-pixels": ["ndarray-pixels@5.2.0", "", { "dependencies": { "@types/ndarray": "^1.0.14", "ndarray": "^1.0.19", "ndarray-ops": "^1.2.2", "sharp": "^0.35.0" } }, "sha512-lTh4tFKziAatVTa9crIsidUyn+lqujVOQpzfdBWvdFu2wo9Uo6z261lVX7SgMyP89xGmj3TMTPbbxl9YDnV4SA=="], + "next": ["next@16.3.0", "", { "dependencies": { "@next/env": "16.3.0", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.0", "@next/swc-darwin-x64": "16.3.0", "@next/swc-linux-arm64-gnu": "16.3.0", "@next/swc-linux-arm64-musl": "16.3.0", "@next/swc-linux-x64-gnu": "16.3.0", "@next/swc-linux-x64-musl": "16.3.0", "@next/swc-win32-arm64-msvc": "16.3.0", "@next/swc-win32-x64-msvc": "16.3.0", "sharp": "^0.35.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A=="], "number-flow": ["number-flow@0.6.2", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-MCnImG4Q5vPwhSXnov56nOuyyKn6LC+Qd7II1UiKc+ACRtug5iAtn0+CwXNxM38AC5lSowEY+oYEtZX2qMnUyw=="], "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "path-expression-matcher": ["path-expression-matcher@1.6.2", "", {}, "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "pdfkit": ["pdfkit@0.19.1", "", { "dependencies": { "@noble/ciphers": "^1.0.0", "@noble/hashes": "^1.6.0", "fontkit": "^2.0.4", "js-md5": "^0.8.3", "linebreak": "^1.1.0", "png-js": "^1.1.0" } }, "sha512-6Gzk+wDwTs4VSxsR5rCMTnIl5nlmkye1oWB0l2hDB1EX6ZNSIBroKQEv+2+fPPn+stVjyqzmsqRJVDfB9fo5DA=="], @@ -402,6 +456,8 @@ "promise-worker-transferable": ["promise-worker-transferable@1.0.4", "", { "dependencies": { "is-promise": "^2.1.0", "lie": "^3.0.2" } }, "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw=="], + "property-graph": ["property-graph@4.1.0", "", {}, "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ=="], + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], @@ -434,13 +490,15 @@ "stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="], + "strnum": ["strnum@2.4.2", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="], "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], - "three": ["three@0.185.1", "", {}, "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg=="], + "three": ["three@0.186.0", "", {}, "sha512-cr/fIM2ddMSVbYVgkfD4jLJv7Fh/8ZTjvo+7gQeSVGUZHxpx9FDwoL5iC7hUz/LiRA8wMbqfnb90xKfm1/HHkQ=="], "three-bvh-csg": ["three-bvh-csg@0.0.18", "", { "peerDependencies": { "three": ">=0.179.0", "three-mesh-bvh": ">=0.9.7" } }, "sha512-M3GCZMmGFgASGuDf+YMamM83nVlD/vdwzVHcYbFxgW+g1S7/nKPiuY00YVHOMbjmJPh8mLevGZL65ItHUuGt2w=="], @@ -468,6 +526,8 @@ "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + "uniq": ["uniq@1.0.1", "", {}, "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA=="], + "urdf-loader": ["urdf-loader@0.13.1", "", { "peerDependencies": { "three": ">=0.152.0" } }, "sha512-bHHoh0Yxrgrw91dPAmVnzKleA8HiVMeHy6xYqOVRw3LuZ2hDOwUiQVCbrzPZckO5S30F1Yym8zLCINN0HfcM3w=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], @@ -484,12 +544,18 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zundo": ["zundo@2.3.0", "", { "peerDependencies": { "zustand": "^4.3.0 || ^5.0.0" } }, "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ=="], "zustand": ["zustand@5.0.14", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g=="], + "@pascal-app/core/zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], + + "@pascal-app/editor/zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], + "@react-three/drei/three-mesh-bvh": ["three-mesh-bvh@0.8.3", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg=="], "browserify-zlib/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], diff --git a/package.json b/package.json index be90476..244a68b 100644 --- a/package.json +++ b/package.json @@ -29,19 +29,19 @@ "urdf-loader": "^0.13.1" }, "peerDependencies": { - "@pascal-app/core": ">=1.0.0-beta.4 <1.0.0", - "@pascal-app/editor": ">=1.0.0-beta.4 <1.0.0", - "@pascal-app/viewer": ">=1.0.0-beta.4 <1.0.0", + "@pascal-app/core": ">=1.0.0-beta.4 <2.0.0", + "@pascal-app/editor": ">=1.0.0-beta.4 <2.0.0", + "@pascal-app/viewer": ">=1.0.0-beta.4 <2.0.0", "@react-three/fiber": "^9", "react": "^18 || ^19", - "three": "^0.185", + "three": "^0.185 || ^0.186", "zod": "^4", "zustand": "^5" }, "devDependencies": { - "@pascal-app/core": "1.0.0-beta.4", - "@pascal-app/editor": "1.0.0-beta.4", - "@pascal-app/viewer": "1.0.0-beta.4", + "@pascal-app/core": "1.0.3", + "@pascal-app/editor": "1.0.3", + "@pascal-app/viewer": "1.0.3", "@react-three/fiber": "^9.5.0", "@types/bun": "^1.3.5", "@types/blob-stream": "^0.1.33", @@ -50,10 +50,10 @@ "@types/pdfkit": "^0.17.3", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", - "@types/three": "^0.185.0", + "@types/three": "0.184.1", "react": "^19.2.4", "react-dom": "^19.2.4", - "three": "^0.185.0", + "three": "0.186.0", "typescript": "6.0.3", "zod": "^4.3.6", "zustand": "^5.0.11" From ce89c7518d4f0afb9406ba033b3b33d0c579cb8c Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sat, 26 Sep 2026 21:01:22 -0400 Subject: [PATCH 3/8] test: inspector slider drags commit one undo step, failing first Co-Authored-By: Claude Opus 5.5 (1M context) --- src/joint-controls.test.ts | 22 ++++++++ src/test-support/host-stubs.ts | 26 ++++++++++ src/test-support/joint-controls-probe.tsx | 61 +++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/joint-controls.test.ts create mode 100644 src/test-support/host-stubs.ts create mode 100644 src/test-support/joint-controls-probe.tsx diff --git a/src/joint-controls.test.ts b/src/joint-controls.test.ts new file mode 100644 index 0000000..f15bd92 --- /dev/null +++ b/src/joint-controls.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from 'bun:test' +import { runProbe } from './test-support/host-stubs' + +describe('Articraft inspector', () => { + test('every slider drag commits as one undo step (restoreOnCommit left on)', () => { + const sliders = runProbe>( + new URL('./test-support/joint-controls-probe.tsx', import.meta.url).pathname, + ) + expect(sliders.map((slider) => slider.label)).toEqual([ + 'Scale', + 'Position X', + 'Position Y', + 'Position Z', + 'Rotation X', + 'Rotation Y', + 'Rotation Z', + 'lid', + 'drawer', + ]) + expect(sliders.filter((slider) => slider.restoreOnCommit === false)).toEqual([]) + }) +}) diff --git a/src/test-support/host-stubs.ts b/src/test-support/host-stubs.ts new file mode 100644 index 0000000..5619cce --- /dev/null +++ b/src/test-support/host-stubs.ts @@ -0,0 +1,26 @@ +import { plugin } from 'bun' + +/** + * Replaces the host packages (`@pascal-app/core`, `editor`, `viewer`) with + * stubs for a probe script. Module stubs are process-global, so probes run in + * a child process (see `runProbe`) and never inside the test runner. + */ +export function stubHostModules(stubs: Record>): void { + plugin({ + name: 'articraft-host-stubs', + setup(build) { + for (const [specifier, exports] of Object.entries(stubs)) { + build.module(specifier, () => ({ exports, loader: 'object' })) + } + }, + }) +} + +/** Runs a probe script in a child process and returns the JSON it prints. */ +export function runProbe(probe: string): T { + const result = Bun.spawnSync([process.execPath, probe], { stderr: 'pipe', stdout: 'pipe' }) + if (result.exitCode !== 0) { + throw new Error(`${probe} failed:\n${result.stderr.toString()}`) + } + return JSON.parse(result.stdout.toString()) as T +} diff --git a/src/test-support/joint-controls-probe.tsx b/src/test-support/joint-controls-probe.tsx new file mode 100644 index 0000000..4cd7808 --- /dev/null +++ b/src/test-support/joint-controls-probe.tsx @@ -0,0 +1,61 @@ +import { renderToStaticMarkup } from 'react-dom/server' +import { stubHostModules } from './host-stubs' + +// Renders the Articraft inspector against stubbed host packages and prints +// the props every SliderControl received. +const NODE_ID = 'articraft_probe' +const node = { + id: NODE_ID, + type: 'articraft:asset', + position: [0, 0, 0], + rotation: [0, 0, 0], + scale: 1, + joints: [ + { + name: 'lid', + type: 'revolute', + parent: 'base', + child: 'lid', + axis: [0, 0, 1], + origin: { xyz: [0, 0, 0], rpy: [0, 0, 0] }, + limits: { lower: 0, upper: 1.5 }, + }, + { + name: 'drawer', + type: 'prismatic', + parent: 'base', + child: 'drawer', + axis: [1, 0, 0], + origin: { xyz: [0, 0, 0], rpy: [0, 0, 0] }, + limits: { lower: 0, upper: 0.4 }, + }, + ], + jointValues: {}, + motionEnabled: false, + attribution: { creator: 'Articraft authors', license: 'CC-BY-4.0', sourceUrl: 'https://x.test' }, +} +const sliders: Array<{ label: string; restoreOnCommit: unknown }> = [] +const state = { nodes: { [NODE_ID]: node }, updateNode: () => undefined } + +stubHostModules({ + '@pascal-app/core': { + useScene: Object.assign((select: (value: typeof state) => unknown) => select(state), { + getState: () => state, + }), + }, + '@pascal-app/editor': { + PanelSection: ({ children }: { children?: unknown }) => children, + SliderControl: (props: { label: string; restoreOnCommit?: boolean }) => { + sliders.push({ label: props.label, restoreOnCommit: props.restoreOnCommit ?? 'default' }) + return null + }, + }, + '@pascal-app/viewer': { + useViewer: (select: (value: unknown) => unknown) => + select({ selection: { selectedIds: [NODE_ID] } }), + }, +}) + +const { default: ArticraftJointControls } = await import('../joint-controls') +renderToStaticMarkup() +console.log(JSON.stringify(sliders)) From 45f2e783f01761350955667a97a3be645937c44e Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sat, 26 Sep 2026 21:02:00 -0400 Subject: [PATCH 4/8] fix: inspector drags record one undo step Every slider passed restoreOnCommit={false}: SliderControl pauses history for the drag and, with the flag off, resumes without a tracked write, so a drag of scale, position, rotation or a joint left no undo step. The default restores the pre-drag value and writes the final one as a single tracked change. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/joint-controls.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/joint-controls.tsx b/src/joint-controls.tsx index d67fe85..e249e4d 100644 --- a/src/joint-controls.tsx +++ b/src/joint-controls.tsx @@ -73,7 +73,6 @@ export default function ArticraftJointControls() { min={0.05} onChange={setScale} precision={2} - restoreOnCommit={false} step={0.05} value={node.scale} /> @@ -85,7 +84,6 @@ export default function ArticraftJointControls() { min={-100} onChange={(value) => setVector('position', index, value)} precision={2} - restoreOnCommit={false} step={0.05} unit="m" value={node.position[index]} @@ -99,7 +97,6 @@ export default function ArticraftJointControls() { min={-180} onChange={(value) => setVector('rotation', index, value)} precision={1} - restoreOnCommit={false} step={1} unit="°" value={toDegrees(node.rotation[index])} @@ -126,7 +123,6 @@ export default function ArticraftJointControls() { min={displayRange[0]} onChange={(value) => setJoint(joint, angular ? toRadians(value) : value)} precision={angular ? 1 : 3} - restoreOnCommit={false} step={angular ? 1 : 0.001} unit={angular ? '°' : 'm'} value={angular From 97952c82f79065900f8d43438154f66d3479662c Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sat, 26 Sep 2026 21:02:43 -0400 Subject: [PATCH 5/8] test: continuous joints spin a full turn per loop, failing first Co-Authored-By: Claude Opus 5.5 (1M context) --- src/animation.test.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ src/motion.test.ts | 12 ++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/animation.test.ts b/src/animation.test.ts index b38ff20..39d27e3 100644 --- a/src/animation.test.ts +++ b/src/animation.test.ts @@ -89,6 +89,49 @@ describe('Articraft GLB animation export', () => { expect(fixedTarget.position.toArray()).toEqual([0, 0, 0]) }) + test('bakes a continuous joint as a full turn over the loop, seamless at the seam', () => { + const root = new Group() + const wheelTarget = new Group() + markArticraftJointTarget(wheelTarget, 'wheel_spin') + root.add(wheelTarget) + const node = ArticraftAssetNode.parse({ + id: 'articraft_spin_test', + catalogId: 'spin-test', + title: 'Spin test', + source: 'generated', + artifact: { format: 'usdz', url: 'https://assets.example/model.usdz' }, + joints: [ + { + name: 'wheel_spin', + type: 'continuous', + parent: 'base', + child: 'wheel', + axis: [0, 1, 0], + origin: { xyz: [0, 0, 0], rpy: [0, 0, 0] }, + limits: null, + }, + ], + attribution: { + creator: 'Articraft authors', + license: 'CC-BY-4.0', + sourceUrl: 'https://example.com/source', + }, + }) + + const track = bakeArticraftAnimation(node, root)?.tracks[0] + expect(track?.ValueTypeName).toBe('quaternion') + const rest = new Quaternion() + const at = (sample: number) => + new Quaternion().fromArray(Array.from(track?.values.slice(sample * 4, sample * 4 + 4) ?? [])) + // 32 samples per loop: a quarter loop is a quarter turn, half a loop half a turn. + expect(at(8).angleTo(rest)).toBeCloseTo(Math.PI / 2, 6) + expect(at(16).angleTo(rest)).toBeCloseTo(Math.PI, 6) + expect(at(24).angleTo(rest)).toBeCloseTo(Math.PI / 2, 6) + expect(at(32).angleTo(rest)).toBeCloseTo(0, 6) + const quarter = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI / 2) + expect(at(8).angleTo(quarter)).toBeCloseTo(0, 6) + }) + test('does not emit an animation without a resolved movable joint target', () => { const node = ArticraftAssetNode.parse({ id: 'articraft_static_test', diff --git a/src/motion.test.ts b/src/motion.test.ts index 97c4dc5..5cefacc 100644 --- a/src/motion.test.ts +++ b/src/motion.test.ts @@ -18,6 +18,18 @@ describe('persistent articulation motion', () => { expect(motionValueAtTime(hinge, 0, Math.PI / 2 / 1.4)).toBeCloseTo(1.5, 8) }) + test('spins a continuous joint one full turn per loop instead of rocking it', () => { + const wheel: ArticraftJoint = { ...hinge, type: 'continuous', limits: null } + const period = (Math.PI * 2) / 1.4 + const samples = Array.from({ length: 9 }, (_, step) => + motionValueAtTime(wheel, 0, (step / 8) * period), + ) + for (const [step, value] of samples.entries()) { + expect(value).toBeCloseTo((step / 8) * Math.PI * 2, 8) + } + expect(motionValueAtTime(wheel, 0, period * 3)).toBeCloseTo(Math.PI * 6, 8) + }) + test('provides a bounded fallback for joints without limits', () => { expect(motionRange({ ...hinge, type: 'continuous', limits: null })).toEqual([ -Math.PI, From cdbc0b14338c1b15f8d3b40746d66c060af73b8b Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sat, 26 Sep 2026 21:03:58 -0400 Subject: [PATCH 6/8] fix: continuous joints spin instead of rocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continuous joints (wheels, fans, knobs: 20.8 % of the catalog's movable joints) followed the same sine sweep as limited ones, over a fallback range of ±π, so they rocked back and forth. They now turn one full revolution per motion period, live and in the exported loop clip, which stays seamless. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/animation.test.ts | 2 +- src/animation.ts | 7 +++---- src/motion.ts | 9 ++++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/animation.test.ts b/src/animation.test.ts index 39d27e3..8d98243 100644 --- a/src/animation.test.ts +++ b/src/animation.test.ts @@ -129,7 +129,7 @@ describe('Articraft GLB animation export', () => { expect(at(24).angleTo(rest)).toBeCloseTo(Math.PI / 2, 6) expect(at(32).angleTo(rest)).toBeCloseTo(0, 6) const quarter = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI / 2) - expect(at(8).angleTo(quarter)).toBeCloseTo(0, 6) + expect(at(8).angleTo(quarter)).toBeLessThan(1e-3) }) test('does not emit an animation without a resolved movable joint target', () => { diff --git a/src/animation.ts b/src/animation.ts index fd2901c..eecdf1e 100644 --- a/src/animation.ts +++ b/src/animation.ts @@ -6,12 +6,11 @@ import { Vector3, VectorKeyframeTrack, } from 'three' -import { motionValueAtTime } from './motion' +import { MOTION_PERIOD_SECONDS, motionValueAtTime } from './motion' import type { ArticraftAssetNode } from './schema' import type { ArticraftJoint } from './types' const JOINT_TARGET_KEY = '__articraftJointTarget' -const MOTION_DURATION_SECONDS = (Math.PI * 2) / 1.4 const MOTION_SAMPLES = 32 type JointTargetMarker = { @@ -36,7 +35,7 @@ export function bakeArticraftAnimation( const tracks = [] as Array const times = Array.from( { length: MOTION_SAMPLES + 1 }, - (_, step) => (step / MOTION_SAMPLES) * MOTION_DURATION_SECONDS, + (_, step) => (step / MOTION_SAMPLES) * MOTION_PERIOD_SECONDS, ) let motionIndex = 0 @@ -66,7 +65,7 @@ export function bakeArticraftAnimation( } if (tracks.length === 0) return null - const clip = new AnimationClip(`${node.id}: loop`, MOTION_DURATION_SECONDS, tracks) + const clip = new AnimationClip(`${node.id}: loop`, MOTION_PERIOD_SECONDS, tracks) clip.userData = { loop: true } return clip } diff --git a/src/motion.ts b/src/motion.ts index 04b9c4d..bfa13cf 100644 --- a/src/motion.ts +++ b/src/motion.ts @@ -39,6 +39,11 @@ export function usePrefersReducedMotion(): boolean { ) } +/** Motion rate in radians per second: the sweep's angular frequency, and how fast continuous joints turn. */ +export const MOTION_RATE = 1.4 +/** One sweep of the limited joints, and one full turn of the continuous ones. */ +export const MOTION_PERIOD_SECONDS = (Math.PI * 2) / MOTION_RATE + export function motionRange(joint: ArticraftJoint): [number, number] { if (joint.limits && joint.limits.upper > joint.limits.lower) { return [joint.limits.lower, joint.limits.upper] @@ -51,6 +56,8 @@ export function motionValueAtTime( index: number, elapsedSeconds: number, ): number { + // A wheel, fan or knob turns without end; sweeping it back and forth over ±π rocks it instead. + if (joint.type === 'continuous') return elapsedSeconds * MOTION_RATE const hasLimits = joint.limits && joint.limits.upper > joint.limits.lower const lower = hasLimits ? joint.limits!.lower @@ -62,6 +69,6 @@ export function motionValueAtTime( : joint.type === 'prismatic' ? 1 : Math.PI - const progress = (Math.sin(elapsedSeconds * 1.4 + index * 0.7) + 1) / 2 + const progress = (Math.sin(elapsedSeconds * MOTION_RATE + index * 0.7) + 1) / 2 return lower + (upper - lower) * progress } From 93a3e10dee7e8de40e4d33fe621b121a8e069e88 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sat, 26 Sep 2026 21:05:16 -0400 Subject: [PATCH 7/8] test: placement saves the rest pose and no playback state, failing first Co-Authored-By: Claude Opus 5.5 (1M context) --- src/catalog-node.test.ts | 42 ++++++++++++++++++++++++++++++++++++++++ src/motion-state.test.ts | 15 ++++++++++++++ src/schema.test.ts | 6 +++--- 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/catalog-node.test.ts create mode 100644 src/motion-state.test.ts diff --git a/src/catalog-node.test.ts b/src/catalog-node.test.ts new file mode 100644 index 0000000..e8a3634 --- /dev/null +++ b/src/catalog-node.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from 'bun:test' +import { catalogNode } from './catalog-node' +import type { ArticraftCatalogItem } from './types' + +const item: ArticraftCatalogItem = { + id: 'articraft-10k:lamp', + title: 'Task lamp', + source: 'articraft-10k', + artifact: { format: 'urdf', url: 'https://assets.example/model.urdf' }, + dimensions: [0.3, 0.6, 0.3], + parts: [{ name: 'base' }, { name: 'arm' }], + joints: [ + { + name: 'arm_hinge', + type: 'revolute', + parent: 'base', + child: 'arm', + axis: [0, 1, 0], + origin: { xyz: [0, 0, 0.1], rpy: [0, 0, 0] }, + limits: { lower: 0, upper: 1.2 }, + }, + ], + defaultJointValues: { arm_hinge: 0 }, + attribution: { + creator: 'Articraft authors', + license: 'CC-BY-4.0', + sourceUrl: 'https://huggingface.co/datasets/camvsl/Articraft-10K', + }, +} + +describe('placing a catalog asset', () => { + test('saves its rest pose and no playback state', () => { + const node = catalogNode(item, [1, 0, 2]) + expect(node).toMatchObject({ + type: 'articraft:asset', + position: [1, 0, 2], + catalogId: item.id, + jointValues: { arm_hinge: 0 }, + }) + expect(Object.hasOwn(node, 'motionEnabled')).toBe(false) + }) +}) diff --git a/src/motion-state.test.ts b/src/motion-state.test.ts new file mode 100644 index 0000000..186ea15 --- /dev/null +++ b/src/motion-state.test.ts @@ -0,0 +1,15 @@ +import { beforeEach, describe, expect, test } from 'bun:test' +import { articraftMotionPlaying, setArticraftMotionPlaying, useArticraftMotion } from './motion-state' + +describe('Articraft playback state', () => { + beforeEach(() => useArticraftMotion.setState({ playing: {} })) + + test('is stopped until started, per asset', () => { + expect(articraftMotionPlaying('articraft_a')).toBe(false) + setArticraftMotionPlaying('articraft_a', true) + expect(articraftMotionPlaying('articraft_a')).toBe(true) + expect(articraftMotionPlaying('articraft_b')).toBe(false) + setArticraftMotionPlaying('articraft_a', false) + expect(useArticraftMotion.getState().playing).toEqual({}) + }) +}) diff --git a/src/schema.test.ts b/src/schema.test.ts index 7e87cff..d881ebf 100644 --- a/src/schema.test.ts +++ b/src/schema.test.ts @@ -36,11 +36,10 @@ describe('Articraft asset schema', () => { expect(node.type).toBe('articraft:asset') expect(node.jointValues.globe_spin).toBe(1.25) - expect(node.motionEnabled).toBe(true) expect(node.artifact.sha256).toBe('a'.repeat(64)) }) - test('keeps older nodes still until motion is explicitly enabled', () => { + test('drops the playback flag older scenes saved: playback is transient', () => { const node = ArticraftAssetNode.parse({ catalogId: 'legacy', title: 'Existing asset', @@ -51,8 +50,9 @@ describe('Articraft asset schema', () => { license: 'CC-BY-4.0', sourceUrl: 'https://huggingface.co/datasets/camvsl/Articraft-10K', }, + motionEnabled: true, }) - expect(node.motionEnabled).toBe(false) + expect(Object.hasOwn(node, 'motionEnabled')).toBe(false) }) }) From 63a872a5c01a35c99307528fc3c6e97fcd2d4a91 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sat, 26 Sep 2026 21:07:03 -0400 Subject: [PATCH 8/8] fix: motion starts off and is never saved Placement wrote motionEnabled: true and the inspector toggled it through updateNode, so every placed asset animated forever by default and Start/Stop went into undo, autosave and collaboration. Playback now lives in a transient per-asset store: assets start still, the scene keeps only the rest pose (jointValues), and stopping returns every joint to it. The saved flag is dropped from the schema; older scenes that saved it load still. The placement preview shows the rest pose too. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 17 ++++---- src/catalog-node.ts | 23 +++++++++++ src/definition.ts | 3 +- src/joint-controls.tsx | 29 ++++++-------- src/motion-state.ts | 24 ++++++++++++ src/renderer.tsx | 17 ++++---- src/schema.test.ts | 1 - src/schema.ts | 1 - src/test-support/joint-controls-probe.tsx | 1 - src/tool.tsx | 48 ++--------------------- 10 files changed, 84 insertions(+), 80 deletions(-) create mode 100644 src/catalog-node.ts create mode 100644 src/motion-state.ts diff --git a/README.md b/README.md index 5887c53..5fce372 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ plugin communicates only through the documented public plugin contracts and same-origin host routes described below. Placement previews use the same articulated URDF/USDZ hierarchy as committed -nodes, with translucent non-interactive materials and live articulation. New -placements keep that motion active after commit and after selection changes. -The inspector can stop or restart motion without writing per-frame joint values -into scene history or autosave. Automatic motion is paused when the operating -system requests reduced motion. +nodes, with translucent non-interactive materials, shown in their rest pose. +Placed assets start still. The inspector starts and stops their motion; playing +is transient, like Pascal's own mechanisms: it is never written to the scene, +scene history, autosave or collaboration, and a reload shows the saved rest +pose. Motion is paused when the operating system requests reduced motion. This is a Pascal integration maintained by `pascalorg`. It is not an official hosted service from the Articraft researchers. @@ -40,8 +40,8 @@ hosted service from the Articraft researchers. without plugin-specific quality toggles. - Live position, rotation, scale, and joint controls in the Pascal inspector; radians stay radians in scene data and are displayed as degrees only for people. -- Persistent, selection-independent motion that smoothly sweeps every movable - joint while keeping per-frame values transient. +- Transient, selection-independent motion: limited joints sweep their range and + continuous joints turn. - Native looping glTF animation clips for those articulated joints when a Pascal project is exported to GLB; the saved joint pose remains the file's rest pose. - A registry-native 2D footprint that follows each asset's persisted position, @@ -71,7 +71,8 @@ credentials. Hosts expose the same-origin broker described in provider may retain inputs under its own policy. - A placed Pascal node persists the catalog or generated artifact URL and digest, title, source attribution, optional generation prompt, dimensions, - part/joint graph, transform, current joint pose, and whether motion is enabled. + part/joint graph, transform and rest joint pose. Whether motion is playing is + not saved. It does not persist a provider key, worker bearer token, Supabase service-role key, or Pascal user identity. diff --git a/src/catalog-node.ts b/src/catalog-node.ts new file mode 100644 index 0000000..6e56414 --- /dev/null +++ b/src/catalog-node.ts @@ -0,0 +1,23 @@ +import { ArticraftAssetNode } from './schema' +import type { ArticraftCatalogItem, Vec3 } from './types' + +/** The node placing a catalog item creates: its artifact, joint graph and rest pose. */ +export function catalogNode(item: ArticraftCatalogItem, position: Vec3): ArticraftAssetNode { + return ArticraftAssetNode.parse({ + name: item.title, + position, + rotation: [0, 0, 0], + scale: 1, + catalogId: item.id, + title: item.title, + source: item.source, + artifact: item.artifact, + thumbnailUrl: item.thumbnailUrl, + dimensions: item.dimensions, + parts: item.parts, + joints: item.joints, + jointValues: item.defaultJointValues, + attribution: item.attribution, + prompt: item.prompt, + }) +} diff --git a/src/definition.ts b/src/definition.ts index 827e800..80554d4 100644 --- a/src/definition.ts +++ b/src/definition.ts @@ -74,7 +74,6 @@ export const articraftAssetDefinition: Definition = { parts: [], joints: [], jointValues: {}, - motionEnabled: false, attribution: { creator: 'Articraft authors', license: 'CC-BY-4.0', @@ -125,6 +124,6 @@ export const articraftAssetDefinition: Definition = { }, mcp: { description: - 'A poseable Articraft asset. Stores an immutable artifact, rigid-part joint graph, per-joint revolute/continuous/prismatic pose values, and persistent motion state.', + 'A poseable Articraft asset. Stores an immutable artifact, rigid-part joint graph and per-joint revolute/continuous/prismatic rest pose values. Playing its motion is transient and never saved.', }, } diff --git a/src/joint-controls.tsx b/src/joint-controls.tsx index e249e4d..187a133 100644 --- a/src/joint-controls.tsx +++ b/src/joint-controls.tsx @@ -5,6 +5,7 @@ import { PanelSection, SliderControl } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useMemo } from 'react' import { usePrefersReducedMotion } from './motion' +import { setArticraftMotionPlaying, useArticraftMotion } from './motion-state' import type { ArticraftAssetNode } from './schema' import type { ArticraftJoint } from './types' @@ -18,6 +19,9 @@ export default function ArticraftJointControls() { ? (value as unknown as ArticraftAssetNode) : null }) + const playing = useArticraftMotion((state) => + node ? state.playing[node.id] === true : false, + ) const reducedMotion = usePrefersReducedMotion() const movable = useMemo( () => node?.joints.filter((joint) => joint.type !== 'fixed') ?? [], @@ -26,27 +30,20 @@ export default function ArticraftJointControls() { if (!node) return null - const setMotionEnabled = (motionEnabled: boolean) => { - useScene.getState().updateNode(node.id as AnyNodeId, { motionEnabled } as never) - } - + // Posing a joint stops playback so the pose being edited is the one shown. const setJoint = (joint: ArticraftJoint, value: number) => { + setArticraftMotionPlaying(node.id, false) useScene.getState().updateNode( node.id as AnyNodeId, - { - motionEnabled: false, - jointValues: { ...node.jointValues, [joint.name]: value }, - } as never, + { jointValues: { ...node.jointValues, [joint.name]: value } } as never, ) } const reset = () => { + setArticraftMotionPlaying(node.id, false) useScene.getState().updateNode( node.id as AnyNodeId, - { - motionEnabled: false, - jointValues: Object.fromEntries(movable.map((joint) => [joint.name, 0])), - } as never, + { jointValues: Object.fromEntries(movable.map((joint) => [joint.name, 0])) } as never, ) } @@ -135,15 +132,15 @@ export default function ArticraftJointControls() { {movable.length > 0 && (
diff --git a/src/motion-state.ts b/src/motion-state.ts new file mode 100644 index 0000000..1077e65 --- /dev/null +++ b/src/motion-state.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand' + +/** + * Which placed assets are playing their motion. Playback is transient, like + * Pascal's own mechanisms: never saved, undone or shared. The scene keeps only + * each asset's rest pose (`jointValues`). + */ +export const useArticraftMotion = create<{ playing: Record }>(() => ({ + playing: {}, +})) + +export function articraftMotionPlaying(nodeId: string): boolean { + return useArticraftMotion.getState().playing[nodeId] === true +} + +export function setArticraftMotionPlaying(nodeId: string, on: boolean): void { + useArticraftMotion.setState((state) => { + if ((state.playing[nodeId] === true) === on) return state + const playing = { ...state.playing } + if (on) playing[nodeId] = true + else delete playing[nodeId] + return { playing } + }) +} diff --git a/src/renderer.tsx b/src/renderer.tsx index c0c7dd0..bb0cf0d 100644 --- a/src/renderer.tsx +++ b/src/renderer.tsx @@ -29,6 +29,7 @@ import URDFLoader from 'urdf-loader' import { markArticraftJointTarget } from './animation' import { createArticraftAppearanceController } from './appearance' import { motionValueAtTime, usePrefersReducedMotion } from './motion' +import { setArticraftMotionPlaying, useArticraftMotion } from './motion-state' import type { ArticraftAssetNode } from './schema' import { resolveArticraftRootTransform } from './transform' import type { ArticraftJoint } from './types' @@ -49,6 +50,8 @@ export default function ArticraftRenderer({ node }: { node: ArticraftAssetNode } const liveOverride = useLiveNodeOverrides((state) => state.overrides.get(node.id)) const transform = resolveArticraftRootTransform(node, liveTransform, liveOverride) useRegistry(node.id, node.type, ref as React.RefObject) + // Playback is transient: it ends with the asset rather than outliving it. + useEffect(() => () => setArticraftMotionPlaying(node.id, false), [node.id]) return ( (null) @@ -81,8 +82,9 @@ export function ArticraftVisual({ const textures = useViewer((state) => state.textures) const colorPreset = useViewer((state) => state.colorPreset) const sceneTheme = useViewer((state) => state.sceneTheme) + const playing = useArticraftMotion((state) => !ghost && state.playing[node.id] === true) const shouldAnimate = - (motionEnabled ?? node.motionEnabled) && + playing && !reducedMotion && node.joints.some((joint) => joint.type !== 'fixed') @@ -141,13 +143,14 @@ export function ArticraftVisual({ invalidate() }, [colorPreset, ghost, invalidate, loaded, sceneTheme, shading, textures]) + // The rest pose, and where every joint returns when playback stops. useEffect(() => { - if (!loaded) return - for (const [name, value] of Object.entries(node.jointValues)) { - loaded.setJointValue(name, value) + if (!loaded || shouldAnimate) return + for (const joint of node.joints) { + if (joint.type !== 'fixed') loaded.setJointValue(joint.name, node.jointValues[joint.name] ?? 0) } invalidate() - }, [invalidate, loaded, node.jointValues, shouldAnimate]) + }, [invalidate, loaded, node.jointValues, node.joints, shouldAnimate]) useFrame((state) => { if (!(loaded && shouldAnimate)) return diff --git a/src/schema.test.ts b/src/schema.test.ts index d881ebf..3ac3599 100644 --- a/src/schema.test.ts +++ b/src/schema.test.ts @@ -26,7 +26,6 @@ describe('Articraft asset schema', () => { }, ], jointValues: { globe_spin: 1.25 }, - motionEnabled: true, attribution: { creator: 'Articraft authors', license: 'CC-BY-4.0', diff --git a/src/schema.ts b/src/schema.ts index 970f4f1..d1b37a4 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -48,7 +48,6 @@ export const ArticraftAssetNode = BaseNode.extend({ .default([]), joints: z.array(ArticraftJoint).default([]), jointValues: z.record(z.string(), z.number()).default({}), - motionEnabled: z.boolean().default(false), attribution: z.object({ creator: z.string(), license: z.string(), diff --git a/src/test-support/joint-controls-probe.tsx b/src/test-support/joint-controls-probe.tsx index 4cd7808..eaf8bf7 100644 --- a/src/test-support/joint-controls-probe.tsx +++ b/src/test-support/joint-controls-probe.tsx @@ -31,7 +31,6 @@ const node = { }, ], jointValues: {}, - motionEnabled: false, attribution: { creator: 'Articraft authors', license: 'CC-BY-4.0', sourceUrl: 'https://x.test' }, } const sliders: Array<{ label: string; restoreOnCommit: unknown }> = [] diff --git a/src/tool.tsx b/src/tool.tsx index 4ab685b..302c509 100644 --- a/src/tool.tsx +++ b/src/tool.tsx @@ -4,58 +4,18 @@ import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core' import { EDITOR_LAYER, triggerSFX } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useMemo } from 'react' +import { catalogNode } from './catalog-node' import { ArticraftVisual } from './renderer' -import { ArticraftAssetNode } from './schema' import { useArticraftPlacement } from './placement' import { useArticraftStore } from './store' export default function ArticraftTool() { const activeLevelId = useViewer((state) => state.selection.levelId) const item = useArticraftStore((state) => state.selectedItem) - const previewNode = useMemo( - () => - item - ? ArticraftAssetNode.parse({ - name: item.title, - position: [0, 0, 0], - rotation: [0, 0, 0], - scale: 1, - catalogId: item.id, - title: item.title, - source: item.source, - artifact: item.artifact, - thumbnailUrl: item.thumbnailUrl, - dimensions: item.dimensions, - parts: item.parts, - joints: item.joints, - jointValues: item.defaultJointValues, - motionEnabled: true, - attribution: item.attribution, - prompt: item.prompt, - }) - : null, - [item], - ) + const previewNode = useMemo(() => (item ? catalogNode(item, [0, 0, 0]) : null), [item]) const { cursorRef, cursorVisible } = useArticraftPlacement(activeLevelId, (position) => { if (!(activeLevelId && item)) return - const node = ArticraftAssetNode.parse({ - name: item.title, - position, - rotation: [0, 0, 0], - scale: 1, - catalogId: item.id, - title: item.title, - source: item.source, - artifact: item.artifact, - thumbnailUrl: item.thumbnailUrl, - dimensions: item.dimensions, - parts: item.parts, - joints: item.joints, - jointValues: item.defaultJointValues, - motionEnabled: true, - attribution: item.attribution, - prompt: item.prompt, - }) + const node = catalogNode(item, position) useScene.getState().createNode(node as unknown as AnyNode, activeLevelId as AnyNodeId) useViewer.getState().setSelection({ selectedIds: [node.id as AnyNodeId] }) triggerSFX('sfx:item-place') @@ -65,7 +25,7 @@ export default function ArticraftTool() { return ( - + ) }