Merge pull request #449 from pascalorg/feat/floorplan-export
feat(editor): floorplan export (multi-page PDF, per level)
This commit is contained in:
@@ -147,10 +147,12 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
|
"jspdf": "^4.2.1",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"motion": "^12.34.3",
|
"motion": "^12.34.3",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
"svg2pdf.js": "^2.7.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"three-mesh-bvh": "~0.9.8",
|
"three-mesh-bvh": "~0.9.8",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
@@ -857,6 +859,10 @@
|
|||||||
|
|
||||||
"@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
|
"@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
|
||||||
|
|
||||||
|
"@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="],
|
||||||
|
|
||||||
|
"@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
@@ -867,6 +873,8 @@
|
|||||||
|
|
||||||
"@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/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/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
"@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="],
|
"@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="],
|
||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
@@ -961,6 +969,8 @@
|
|||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
|
||||||
|
|
||||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.35", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.35", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg=="],
|
||||||
@@ -995,6 +1005,8 @@
|
|||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="],
|
||||||
|
|
||||||
|
"canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="],
|
||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
||||||
@@ -1035,12 +1047,18 @@
|
|||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="],
|
||||||
|
|
||||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||||
|
|
||||||
"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-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=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
|
||||||
|
|
||||||
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||||
@@ -1075,6 +1093,8 @@
|
|||||||
|
|
||||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
|
"dompurify": ["dompurify@3.4.11", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw=="],
|
||||||
|
|
||||||
"dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
|
"dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
|
||||||
|
|
||||||
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||||
@@ -1173,6 +1193,8 @@
|
|||||||
|
|
||||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
|
"fast-png": ["fast-png@6.4.0", "", { "dependencies": { "@types/pako": "^2.0.3", "iobuffer": "^5.3.2", "pako": "^2.1.0" } }, "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q=="],
|
||||||
|
|
||||||
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
||||||
|
|
||||||
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
||||||
@@ -1201,6 +1223,8 @@
|
|||||||
|
|
||||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||||
|
|
||||||
|
"font-family-papandreou": ["font-family-papandreou@0.2.0-patch2", "", {}, "sha512-l/YiRdBSH/eWv6OF3sLGkwErL+n0MqCICi9mppTZBOCL5vixWGDqCYvRcuxB2h7RGCTzaTKOHT2caHvCXQPRlw=="],
|
||||||
|
|
||||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||||
|
|
||||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
@@ -1269,6 +1293,8 @@
|
|||||||
|
|
||||||
"howler": ["howler@2.2.4", "", {}, "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="],
|
"howler": ["howler@2.2.4", "", {}, "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="],
|
||||||
|
|
||||||
|
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
|
||||||
|
|
||||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
@@ -1295,6 +1321,8 @@
|
|||||||
|
|
||||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||||
|
|
||||||
|
"iobuffer": ["iobuffer@5.4.0", "", {}, "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="],
|
||||||
|
|
||||||
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
||||||
|
|
||||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
@@ -1385,6 +1413,8 @@
|
|||||||
|
|
||||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||||
|
|
||||||
|
"jspdf": ["jspdf@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.3.1", "html2canvas": "^1.0.0-rc.5" } }, "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ=="],
|
||||||
|
|
||||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||||
|
|
||||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
@@ -1541,6 +1571,8 @@
|
|||||||
|
|
||||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||||
|
|
||||||
|
"pako": ["pako@2.2.0", "", {}, "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w=="],
|
||||||
|
|
||||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||||
|
|
||||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
@@ -1557,6 +1589,8 @@
|
|||||||
|
|
||||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||||
@@ -1589,6 +1623,8 @@
|
|||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
|
"raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="],
|
||||||
|
|
||||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
|
|
||||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
@@ -1615,6 +1651,8 @@
|
|||||||
|
|
||||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||||
|
|
||||||
|
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
|
||||||
|
|
||||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
@@ -1629,6 +1667,8 @@
|
|||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
|
"rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="],
|
||||||
|
|
||||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||||
|
|
||||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
@@ -1677,6 +1717,10 @@
|
|||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"specificity": ["specificity@0.4.1", "", { "bin": { "specificity": "./bin/specificity" } }, "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg=="],
|
||||||
|
|
||||||
|
"stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="],
|
||||||
|
|
||||||
"stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="],
|
"stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="],
|
||||||
|
|
||||||
"stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="],
|
"stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="],
|
||||||
@@ -1715,6 +1759,12 @@
|
|||||||
|
|
||||||
"suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="],
|
"suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="],
|
||||||
|
|
||||||
|
"svg-pathdata": ["svg-pathdata@6.0.3", "", {}, "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="],
|
||||||
|
|
||||||
|
"svg2pdf.js": ["svg2pdf.js@2.7.0", "", { "dependencies": { "cssesc": "^3.0.0", "font-family-papandreou": "^0.2.0-patch1", "specificity": "^0.4.1", "svgpath": "^2.3.0" }, "peerDependencies": { "jspdf": "^4.0.0 || ^3.0.0 || ^2.0.0" } }, "sha512-nXK4Wx28H0KtOktanm5nsphl1KMEoLNMelAT/776qxPAj9DshwYcqgdpKuBnY1nrcYOriQFHVQLE4tIag+aDJA=="],
|
||||||
|
|
||||||
|
"svgpath": ["svgpath@2.6.0", "", {}, "sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg=="],
|
||||||
|
|
||||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
||||||
@@ -1723,6 +1773,8 @@
|
|||||||
|
|
||||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||||
|
|
||||||
|
"text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="],
|
||||||
|
|
||||||
"three": ["three@0.184.0", "", {}, "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg=="],
|
"three": ["three@0.184.0", "", {}, "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
@@ -1799,6 +1851,8 @@
|
|||||||
|
|
||||||
"utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="],
|
"utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="],
|
||||||
|
|
||||||
|
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
|
||||||
|
|
||||||
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|||||||
@@ -43,14 +43,16 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
|
"jspdf": "^4.2.1",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"motion": "^12.34.3",
|
"motion": "^12.34.3",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
"svg2pdf.js": "^2.7.0",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"three-mesh-bvh": "~0.9.8",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
"zustand": "^5.0.11",
|
"zustand": "^5.0.11"
|
||||||
"three-mesh-bvh": "~0.9.8"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@pascal-app/core": "^0.9.1",
|
"@pascal-app/core": "^0.9.1",
|
||||||
|
|||||||
@@ -1488,7 +1488,7 @@ function buildFloorplanEntryGeometry({
|
|||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFloorplanLevelData(
|
export function getFloorplanLevelData(
|
||||||
type: string,
|
type: string,
|
||||||
nodes: Record<string, AnyNode>,
|
nodes: Record<string, AnyNode>,
|
||||||
liveOverrides: Map<string, LiveNodeOverrides>,
|
liveOverrides: Map<string, LiveNodeOverrides>,
|
||||||
@@ -2426,7 +2426,7 @@ function applyPositionLiveTransform(
|
|||||||
} as AnyNode
|
} as AnyNode
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFloorplanNodeVisible(node: AnyNode, liveOverride?: LiveNodeOverrides): boolean {
|
export function isFloorplanNodeVisible(node: AnyNode, liveOverride?: LiveNodeOverrides): boolean {
|
||||||
const overrideVisible = liveOverride?.visible
|
const overrideVisible = liveOverride?.visible
|
||||||
if (typeof overrideVisible === 'boolean') return overrideVisible
|
if (typeof overrideVisible === 'boolean') return overrideVisible
|
||||||
return (node as { visible?: boolean }).visible !== false
|
return (node as { visible?: boolean }).visible !== false
|
||||||
@@ -2452,7 +2452,7 @@ function isFloorplanHierarchyVisible(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildContext(
|
export function buildContext(
|
||||||
node: AnyNode,
|
node: AnyNode,
|
||||||
nodes: Record<string, AnyNode>,
|
nodes: Record<string, AnyNode>,
|
||||||
viewState: {
|
viewState: {
|
||||||
@@ -2560,7 +2560,7 @@ const OVERLAY_KINDS = new Set<FloorplanGeometry['kind']>([
|
|||||||
* / translations apply in both passes. Empty groups collapse to `null`
|
* / translations apply in both passes. Empty groups collapse to `null`
|
||||||
* so the caller can skip emitting an `<g>` when there's nothing to draw.
|
* so the caller can skip emitting an `<g>` when there's nothing to draw.
|
||||||
*/
|
*/
|
||||||
function splitFloorplanOverlay(g: FloorplanGeometry): {
|
export function splitFloorplanOverlay(g: FloorplanGeometry): {
|
||||||
base: FloorplanGeometry | null
|
base: FloorplanGeometry | null
|
||||||
overlay: FloorplanGeometry | null
|
overlay: FloorplanGeometry | null
|
||||||
} {
|
} {
|
||||||
@@ -2726,7 +2726,7 @@ function depsValueEqual(a: unknown, b: unknown): boolean {
|
|||||||
* Sort is stable in modern JS engines, so siblings within the same
|
* Sort is stable in modern JS engines, so siblings within the same
|
||||||
* bucket keep their DFS order (= scene tree order).
|
* bucket keep their DFS order (= scene tree order).
|
||||||
*/
|
*/
|
||||||
function floorplanLayerRank(type: string): number {
|
export function floorplanLayerRank(type: string): number {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'zone':
|
case 'zone':
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { emitter, useScene, validateBuildJson } from '@pascal-app/core'
|
import { emitter, useScene, validateBuildJson } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { TreeView, VisualJson } from '@visual-json/react'
|
import { TreeView, VisualJson } from '@visual-json/react'
|
||||||
import { Camera, Download, Save, Trash2, Upload } from 'lucide-react'
|
import { Camera, Download, Map as MapIcon, Save, Trash2, Upload } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type SyntheticEvent,
|
type SyntheticEvent,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
import { exportFloorplanPdf } from '../../../../../lib/floorplan/floorplan-export'
|
||||||
import { Button } from './../../../../../components/ui/primitives/button'
|
import { Button } from './../../../../../components/ui/primitives/button'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -344,32 +345,56 @@ export function SettingsPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Export Section */}
|
{/* Export Section */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<label className="font-medium text-muted-foreground text-xs uppercase">Export</label>
|
<label className="font-medium text-muted-foreground text-xs uppercase">Export</label>
|
||||||
<Button
|
|
||||||
className="w-full justify-start gap-2"
|
<div className="space-y-2">
|
||||||
onClick={() => exportScene?.('glb')}
|
<div className="font-medium text-muted-foreground text-xs">3D model</div>
|
||||||
variant="outline"
|
<Button
|
||||||
>
|
className="w-full justify-start gap-2"
|
||||||
<Download className="size-4" />
|
onClick={() => exportScene?.('glb')}
|
||||||
Export GLB
|
variant="outline"
|
||||||
</Button>
|
>
|
||||||
<Button
|
<Download className="size-4" />
|
||||||
className="w-full justify-start gap-2"
|
Export GLB
|
||||||
onClick={() => exportScene?.('stl')}
|
</Button>
|
||||||
variant="outline"
|
<Button
|
||||||
>
|
className="w-full justify-start gap-2"
|
||||||
<Download className="size-4" />
|
onClick={() => exportScene?.('stl')}
|
||||||
Export STL
|
variant="outline"
|
||||||
</Button>
|
>
|
||||||
<Button
|
<Download className="size-4" />
|
||||||
className="w-full justify-start gap-2"
|
Export STL
|
||||||
onClick={() => exportScene?.('obj')}
|
</Button>
|
||||||
variant="outline"
|
<Button
|
||||||
>
|
className="w-full justify-start gap-2"
|
||||||
<Download className="size-4" />
|
onClick={() => exportScene?.('obj')}
|
||||||
Export OBJ
|
variant="outline"
|
||||||
</Button>
|
>
|
||||||
|
<Download className="size-4" />
|
||||||
|
Export OBJ
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium text-muted-foreground text-xs">Floorplan</div>
|
||||||
|
<Button
|
||||||
|
className="w-full justify-start gap-2"
|
||||||
|
onClick={() => exportFloorplanPdf('full')}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<MapIcon className="size-4" />
|
||||||
|
Full floorplan
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full justify-start gap-2"
|
||||||
|
onClick={() => exportFloorplanPdf('structure')}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<MapIcon className="size-4" />
|
||||||
|
Structure only
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Thumbnail Section (only for cloud projects) */}
|
{/* Thumbnail Section (only for cloud projects) */}
|
||||||
|
|||||||
@@ -0,0 +1,348 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
type AnyNode,
|
||||||
|
type AnyNodeId,
|
||||||
|
type FloorplanGeometry,
|
||||||
|
type LiveNodeOverrides,
|
||||||
|
nodeRegistry,
|
||||||
|
resolveBuildingForLevel,
|
||||||
|
useScene,
|
||||||
|
} from '@pascal-app/core'
|
||||||
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
|
import { createElement } from 'react'
|
||||||
|
import { flushSync } from 'react-dom'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { FloorplanGeometryRenderer } from '../../components/editor-2d/renderers/floorplan-geometry-renderer'
|
||||||
|
import {
|
||||||
|
buildContext,
|
||||||
|
floorplanLayerRank,
|
||||||
|
getFloorplanLevelData,
|
||||||
|
isFloorplanNodeVisible,
|
||||||
|
splitFloorplanOverlay,
|
||||||
|
} from '../../components/editor-2d/renderers/floorplan-registry-layer'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Floorplan PDF export.
|
||||||
|
*
|
||||||
|
* Re-runs the same registry-driven geometry pipeline the live 2D layer uses
|
||||||
|
* (`def.floorplan(node, ctx)` → `FloorplanGeometryRenderer`) headlessly, with
|
||||||
|
* a neutral `viewState` so nodes render in their default, unselected form.
|
||||||
|
* Every level of the active building becomes its own page, titled with the
|
||||||
|
* level's label, with the plan fit to the page (independent of the live
|
||||||
|
* pan/zoom). jsPDF + svg2pdf are dynamically imported so they only load when
|
||||||
|
* an export actually runs.
|
||||||
|
*
|
||||||
|
* `scope: 'structure'` keeps only `category === 'structure'` nodes (walls,
|
||||||
|
* slabs, ceilings, doors, windows, stairs, columns, roofs…); `'full'` keeps
|
||||||
|
* every node that has a floorplan builder and is visible.
|
||||||
|
*/
|
||||||
|
export type FloorplanExportScope = 'full' | 'structure'
|
||||||
|
|
||||||
|
const SVG_NS = 'http://www.w3.org/2000/svg'
|
||||||
|
/** Meters of margin around the plan bounds. */
|
||||||
|
const PADDING_M = 1
|
||||||
|
/** PDF page margin + title band, in pt. */
|
||||||
|
const PAGE_MARGIN_PT = 36
|
||||||
|
const TITLE_BAND_PT = 28
|
||||||
|
|
||||||
|
// Neutral view state — no selection / hover / palette, so builders emit their
|
||||||
|
// default appearance (the core palette only carries selection/handle colors).
|
||||||
|
const NEUTRAL_VIEW_STATE = {
|
||||||
|
selected: false,
|
||||||
|
highlighted: false,
|
||||||
|
hovered: false,
|
||||||
|
moving: false,
|
||||||
|
palette: undefined,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type ExportLevel = { id: AnyNodeId; label: string }
|
||||||
|
|
||||||
|
export async function exportFloorplanPdf(scope: FloorplanExportScope): Promise<void> {
|
||||||
|
const nodes = useScene.getState().nodes
|
||||||
|
const levels = resolveExportLevels(nodes)
|
||||||
|
if (levels.length === 0) {
|
||||||
|
console.warn('[floorplan-export] no level to export')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [{ jsPDF }, { svg2pdf }] = await Promise.all([import('jspdf'), import('svg2pdf.js')])
|
||||||
|
const doc = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4' })
|
||||||
|
const pageW = doc.internal.pageSize.getWidth()
|
||||||
|
const pageH = doc.internal.pageSize.getHeight()
|
||||||
|
|
||||||
|
const host = document.createElement('div')
|
||||||
|
host.style.cssText =
|
||||||
|
'position:fixed;left:-10000px;top:0;width:1px;height:1px;overflow:hidden;pointer-events:none;'
|
||||||
|
document.body.appendChild(host)
|
||||||
|
|
||||||
|
let pageCount = 0
|
||||||
|
try {
|
||||||
|
for (const level of levels) {
|
||||||
|
const geometries = collectFloorplanGeometry(nodes, level.id, scope)
|
||||||
|
if (geometries.length === 0) continue
|
||||||
|
|
||||||
|
const mounted = await mountFloorplanSvg(host, geometries)
|
||||||
|
if (!mounted) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (pageCount > 0) doc.addPage()
|
||||||
|
pageCount++
|
||||||
|
|
||||||
|
doc.setFontSize(14)
|
||||||
|
doc.text(level.label, PAGE_MARGIN_PT, PAGE_MARGIN_PT + 12)
|
||||||
|
|
||||||
|
// Fit the plan into the page below the title band, preserving aspect.
|
||||||
|
const boxX = PAGE_MARGIN_PT
|
||||||
|
const boxY = PAGE_MARGIN_PT + TITLE_BAND_PT
|
||||||
|
const boxW = pageW - PAGE_MARGIN_PT * 2
|
||||||
|
const boxH = pageH - PAGE_MARGIN_PT * 2 - TITLE_BAND_PT
|
||||||
|
const aspect = mounted.width / mounted.height
|
||||||
|
let w = boxW
|
||||||
|
let h = w / aspect
|
||||||
|
if (h > boxH) {
|
||||||
|
h = boxH
|
||||||
|
w = h * aspect
|
||||||
|
}
|
||||||
|
const x = boxX + (boxW - w) / 2
|
||||||
|
const y = boxY + (boxH - h) / 2
|
||||||
|
|
||||||
|
// svg2pdf doesn't honour `vector-effect: non-scaling-stroke` (which
|
||||||
|
// many builders use to keep door/window/stair line weights constant
|
||||||
|
// on screen). Left as-is, those pixel-sized widths render as
|
||||||
|
// metre-wide strokes — huge grey blobs. Convert them to the real-unit
|
||||||
|
// width that lands at the intended point weight once svg2pdf scales
|
||||||
|
// the plan onto the page.
|
||||||
|
inlineNonScalingStrokes(mounted.svg, w / mounted.width)
|
||||||
|
|
||||||
|
await svg2pdf(mounted.svg, doc, { x, y, width: w, height: h })
|
||||||
|
} finally {
|
||||||
|
mounted.cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageCount === 0) {
|
||||||
|
console.warn(`[floorplan-export] nothing to export for scope "${scope}"`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date().toISOString().split('T')[0]
|
||||||
|
doc.save(`floorplan_${scope}_${date}.pdf`)
|
||||||
|
} finally {
|
||||||
|
host.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MountedFloorplan = {
|
||||||
|
svg: SVGSVGElement
|
||||||
|
/** Padded viewBox dimensions, in meters — used for aspect-preserving fit. */
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
cleanup: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountFloorplanSvg(
|
||||||
|
parent: HTMLElement,
|
||||||
|
geometries: { id: AnyNodeId; base: FloorplanGeometry }[],
|
||||||
|
): Promise<MountedFloorplan | null> {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
parent.appendChild(container)
|
||||||
|
const root = createRoot(container)
|
||||||
|
const cleanup = () => {
|
||||||
|
root.unmount()
|
||||||
|
container.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render a full `<svg>` as the React root child so React enters the SVG
|
||||||
|
// namespace at the `<svg>` tag, then mutate the DOM node afterwards —
|
||||||
|
// viewBox/background depend on the post-mount measured bounds.
|
||||||
|
flushSync(() => {
|
||||||
|
root.render(
|
||||||
|
createElement(
|
||||||
|
'svg',
|
||||||
|
{ xmlns: SVG_NS },
|
||||||
|
createElement(
|
||||||
|
'g',
|
||||||
|
{ 'data-floorplan-content': '' },
|
||||||
|
geometries.map(({ id, base }) =>
|
||||||
|
createElement(FloorplanGeometryRenderer, { key: id, geometry: base }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Give async asset images (item icons) a couple of frames to resolve so
|
||||||
|
// they're included in the measured bounds and the rendered output.
|
||||||
|
await nextFrames(2)
|
||||||
|
|
||||||
|
const svg = container.querySelector('svg')
|
||||||
|
const content = svg?.querySelector('[data-floorplan-content]') as SVGGraphicsElement | null
|
||||||
|
const bbox = content?.getBBox()
|
||||||
|
if (!svg || !bbox || bbox.width === 0 || bbox.height === 0) {
|
||||||
|
cleanup()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const minX = bbox.x - PADDING_M
|
||||||
|
const minY = bbox.y - PADDING_M
|
||||||
|
const width = bbox.width + PADDING_M * 2
|
||||||
|
const height = bbox.height + PADDING_M * 2
|
||||||
|
svg.setAttribute('viewBox', `${minX} ${minY} ${width} ${height}`)
|
||||||
|
svg.setAttribute('width', `${width}`)
|
||||||
|
svg.setAttribute('height', `${height}`)
|
||||||
|
|
||||||
|
const background = document.createElementNS(SVG_NS, 'rect')
|
||||||
|
background.setAttribute('x', `${minX}`)
|
||||||
|
background.setAttribute('y', `${minY}`)
|
||||||
|
background.setAttribute('width', `${width}`)
|
||||||
|
background.setAttribute('height', `${height}`)
|
||||||
|
background.setAttribute('fill', '#ffffff')
|
||||||
|
svg.insertBefore(background, svg.firstChild)
|
||||||
|
|
||||||
|
return { svg, width, height, cleanup }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bake `vector-effect: non-scaling-stroke` widths into real user units.
|
||||||
|
*
|
||||||
|
* svg2pdf ignores the non-scaling hint, so a `stroke-width="1.25"` meant as
|
||||||
|
* "1.25 screen px" would otherwise render as 1.25 metres on the page. We
|
||||||
|
* rewrite each such width (and any dash pattern) to `px / ptPerUnit` so it
|
||||||
|
* lands at ~`px` points once svg2pdf scales the plan by `ptPerUnit`, then drop
|
||||||
|
* the now-misleading attribute.
|
||||||
|
*/
|
||||||
|
function inlineNonScalingStrokes(svg: SVGSVGElement, ptPerUnit: number) {
|
||||||
|
if (!Number.isFinite(ptPerUnit) || ptPerUnit <= 0) return
|
||||||
|
for (const el of svg.querySelectorAll('[vector-effect="non-scaling-stroke"]')) {
|
||||||
|
const sw = el.getAttribute('stroke-width')
|
||||||
|
if (sw) {
|
||||||
|
const px = Number.parseFloat(sw)
|
||||||
|
if (Number.isFinite(px)) el.setAttribute('stroke-width', `${px / ptPerUnit}`)
|
||||||
|
}
|
||||||
|
const dash = el.getAttribute('stroke-dasharray')
|
||||||
|
if (dash) {
|
||||||
|
const scaled = dash
|
||||||
|
.split(/[\s,]+/)
|
||||||
|
.map((v) => {
|
||||||
|
const n = Number.parseFloat(v)
|
||||||
|
return Number.isFinite(n) ? `${n / ptPerUnit}` : v
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
el.setAttribute('stroke-dasharray', scaled)
|
||||||
|
}
|
||||||
|
el.removeAttribute('vector-effect')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFloorplanGeometry(
|
||||||
|
nodes: Record<string, AnyNode>,
|
||||||
|
levelId: AnyNodeId,
|
||||||
|
scope: FloorplanExportScope,
|
||||||
|
): { id: AnyNodeId; base: FloorplanGeometry }[] {
|
||||||
|
const noLiveOverrides = new Map<string, LiveNodeOverrides>()
|
||||||
|
const levelNodeIdsByType = new Map<string, AnyNodeId[]>()
|
||||||
|
const entries: { id: AnyNodeId; node: AnyNode }[] = []
|
||||||
|
|
||||||
|
const visit = (id: AnyNodeId) => {
|
||||||
|
const node = nodes[id]
|
||||||
|
if (!node) return
|
||||||
|
const def = nodeRegistry.get(node.type)
|
||||||
|
if (def?.computeFloorplanLevelData) {
|
||||||
|
const ids = levelNodeIdsByType.get(node.type)
|
||||||
|
if (ids) ids.push(id)
|
||||||
|
else levelNodeIdsByType.set(node.type, [id])
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
def?.floorplan &&
|
||||||
|
isFloorplanNodeVisible(node) &&
|
||||||
|
(scope === 'full' || def.category === 'structure')
|
||||||
|
) {
|
||||||
|
entries.push({ id, node })
|
||||||
|
}
|
||||||
|
const childIds = (node as { children?: AnyNodeId[] }).children
|
||||||
|
if (Array.isArray(childIds)) for (const cid of childIds) visit(cid)
|
||||||
|
}
|
||||||
|
visit(levelId)
|
||||||
|
|
||||||
|
// Document order is paint order — sort the same way the live layer does so
|
||||||
|
// zones sit under walls/slabs/furniture rather than on top of them.
|
||||||
|
entries.sort((a, b) => floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type))
|
||||||
|
|
||||||
|
// One-shot per-type cache for `computeFloorplanLevelData`; value type is
|
||||||
|
// module-private to the registry layer, so let it infer.
|
||||||
|
const levelDataCache = new Map()
|
||||||
|
const out: { id: AnyNodeId; base: FloorplanGeometry }[] = []
|
||||||
|
for (const { id, node } of entries) {
|
||||||
|
const builder = nodeRegistry.get(node.type)?.floorplan
|
||||||
|
if (!builder) continue
|
||||||
|
const levelData = getFloorplanLevelData(
|
||||||
|
node.type,
|
||||||
|
nodes,
|
||||||
|
noLiveOverrides,
|
||||||
|
levelNodeIdsByType,
|
||||||
|
levelDataCache,
|
||||||
|
)
|
||||||
|
const ctx = buildContext(node, nodes, NEUTRAL_VIEW_STATE, levelData)
|
||||||
|
const geometry = builder(node, ctx)
|
||||||
|
if (!geometry) continue
|
||||||
|
const { base } = splitFloorplanOverlay(geometry)
|
||||||
|
if (base) out.push({ id, base })
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Levels to export, ordered bottom-to-top. The active building (the building
|
||||||
|
* owning the selected level, or the first one found) contributes all of its
|
||||||
|
* level children; if there is no building wrapper we fall back to the single
|
||||||
|
* resolved level.
|
||||||
|
*/
|
||||||
|
function resolveExportLevels(nodes: Record<string, AnyNode>): ExportLevel[] {
|
||||||
|
const selected = useViewer.getState().selection.levelId as AnyNodeId | null | undefined
|
||||||
|
const activeLevelId = selected && nodes[selected] ? selected : firstLevelId(nodes)
|
||||||
|
if (!activeLevelId) return []
|
||||||
|
|
||||||
|
const buildingId = resolveBuildingForLevel(activeLevelId, nodes as Record<AnyNodeId, AnyNode>)
|
||||||
|
let levelNodes: AnyNode[]
|
||||||
|
if (buildingId) {
|
||||||
|
const childIds = (nodes[buildingId] as { children?: AnyNodeId[] }).children ?? []
|
||||||
|
levelNodes = childIds.map((id) => nodes[id]).filter((n): n is AnyNode => n?.type === 'level')
|
||||||
|
} else {
|
||||||
|
const node = nodes[activeLevelId]
|
||||||
|
levelNodes = node ? [node] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
levelNodes.sort((a, b) => levelIndexOf(a) - levelIndexOf(b))
|
||||||
|
return levelNodes.map((n) => ({ id: n.id as AnyNodeId, label: levelLabelOf(n) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstLevelId(nodes: Record<string, AnyNode>): AnyNodeId | null {
|
||||||
|
for (const node of Object.values(nodes)) {
|
||||||
|
if (node.type === 'level') return node.id as AnyNodeId
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function levelIndexOf(node: AnyNode): number {
|
||||||
|
return (node as { level?: number }).level ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function levelLabelOf(node: AnyNode): string {
|
||||||
|
const name = node.name?.trim()
|
||||||
|
if (name) return name
|
||||||
|
return `Level ${levelIndexOf(node)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextFrames(count: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const tick = (remaining: number) => {
|
||||||
|
if (remaining <= 0) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => tick(remaining - 1))
|
||||||
|
}
|
||||||
|
tick(count)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user