更新图钉 API 文档及代码

This commit is contained in:
yuding
2026-04-24 11:16:37 +08:00
parent aeb4c990ad
commit 8db06785eb
80 changed files with 24982 additions and 18738 deletions

View File

@@ -163,7 +163,6 @@ export default function ControlPanel({
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
flex: 1 1 auto;
} }
button:hover { button:hover {
background: #f0f0f0; background: #f0f0f0;

View File

@@ -461,7 +461,6 @@ button {
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
flex: 1 1 auto;
} }
button:hover { button:hover {

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M31.6062 5.51994L31.6062 5.56835L31.6062 5.39893C31.6062 5.44249 31.6062 5.48122 31.6062 5.52478C31.6062 5.51994 31.6062 5.5151 31.6062 5.51026C31.6062 5.51026 31.6062 5.5151 31.6062 5.51994Z" fill="#FFFFFF" ></path><path d="M47 24C47 21.2305 45.7237 18.6513 43.5889 16.9719C45.7237 15.2926 47 12.7134 47 9.94392C47 5.0127 42.9856 1 38.0524 1C35.2865 1 32.7108 2.27108 31.0308 4.40036C29.3462 2.27108 26.7752 1 24.0046 1C21.234 1 18.6537 2.27571 16.9738 4.40964C15.2938 2.27571 12.7135 1 9.94289 1C5.01432 1 1 5.0127 1 9.94392C1 12.7134 2.27623 15.2926 4.41101 16.9719C2.27623 18.6513 1 21.2305 1 24C1 26.7694 2.27623 29.3487 4.41101 31.028C2.27623 32.7073 1 35.2866 1 38.056C1 42.9872 5.01432 47 9.94753 47C12.7181 47 15.2984 45.7242 16.9784 43.5903C18.6584 45.7242 21.2387 47 24.0093 47C26.7752 47 29.3509 45.7289 31.0308 43.5996C32.7108 45.7242 35.2865 47 38.0524 47C42.9856 47 47 42.9872 47 38.056C47 35.2866 45.7237 32.7073 43.5889 31.028C45.7237 29.3487 47 26.7694 47 24ZM41.4124 31.863C43.6864 33.097 45.0972 35.4721 45.0972 38.056C45.0972 41.9388 41.9368 45.098 38.0524 45.098C35.4675 45.098 33.096 43.6877 31.8615 41.4146L31.0401 39.907L31.0262 39.8791L30.9612 40.0044L30.1908 41.4193C30.1816 41.4378 30.1676 41.461 30.1537 41.4796L30.1305 41.5213L30.1444 41.5306C28.8914 43.7388 26.5524 45.1026 24.0139 45.1026C21.429 45.1026 19.0575 43.6924 17.8184 41.4193L16.983 39.8791L16.1477 41.4193C14.9132 43.6924 12.5371 45.1026 9.95217 45.1026C6.06779 45.1026 2.90739 41.9435 2.90739 38.0607C2.90739 35.4768 4.3182 33.1063 6.59221 31.8677L8.13296 31.0326L6.59221 30.1976C4.3182 28.9637 2.90739 26.5885 2.90739 24.0046C2.90739 21.4207 4.3182 19.0502 6.59221 17.8116L8.13296 16.9766L6.59221 16.1416C4.3182 14.9076 2.90739 12.5325 2.90739 9.94856C2.90739 6.06575 6.06779 2.90661 9.95217 2.90661C12.5371 2.90661 14.9086 4.31686 16.1477 6.58995L16.983 8.13008L17.8184 6.58995C19.0528 4.31686 21.429 2.90661 24.0139 2.90661C26.5571 2.90661 28.8914 4.27047 30.1444 6.47861L30.1305 6.48789L30.1584 6.53428C30.1676 6.55284 30.1816 6.57603 30.1908 6.59459L30.9612 8.00947L31.0262 8.13472L31.0401 8.10689L31.8615 6.59923C33.096 4.32614 35.4721 2.91589 38.0571 2.91589C41.9414 2.91589 45.1019 6.07503 45.1019 9.95784C45.1019 12.5417 43.691 14.9122 41.417 16.1509L39.8763 16.9859L41.417 17.8209C43.691 19.0548 45.1019 21.43 45.1019 24.0139C45.1019 26.5978 43.691 28.9683 41.417 30.2069L39.8763 31.0419L41.4124 31.863Z" stroke="rgba(44, 44, 44, 1)" stroke-width="1" fill="#2C2C2C" ></path><path d="M31.6062 5.51994L31.6062 5.56835L31.6062 5.39893C31.6062 5.44249 31.6062 5.48122 31.6062 5.52478C31.6062 5.51994 31.6062 5.5151 31.6062 5.51026C31.6062 5.51026 31.6062 5.5151 31.6062 5.51994Z" fill="#000000" ></path></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M19.0032 32.1184C18.712 32.1184 18.4032 32.1184 18.112 32.4096C17.512 32.6992 17.8224 33.5904 18.112 33.8816L23.1472 38.6256C23.7472 38.9152 24.6192 38.9152 24.928 38.6256L29.9632 33.8816C30.2528 33.5904 30.2528 33.2816 30.2528 32.9904C30.2528 32.3904 29.6528 32.1008 29.072 32.1008L25.7856 32.1008L25.7856 16.2192L29.0256 16.2192C29.3152 16.2192 29.6256 16.2192 29.9152 15.928C30.5152 15.6368 30.2064 14.7472 29.9152 14.456L24.8816 9.7072C24.2816 9.4176 23.4096 9.4176 23.0992 9.7072L18.0656 14.4512C17.7744 14.7424 17.7744 15.0512 17.7744 15.3424C17.7744 15.9424 18.3744 16.232 18.9568 16.232L22.24 16.232L22.24 32.1328L19.0016 32.1328L19.0016 32.1184L19.0032 32.1184ZM4.75842 6.80639L43.2384 6.80639C44.128 6.80639 44.7104 6.20639 44.7104 5.33442C44.7104 4.1536 44.1088 3.2624 43.2384 3.2624L4.75678 3.2624C4.1568 3.2624 3.5744 3.8624 3.2848 4.73442C3.2848 5.62561 3.8848 6.5152 4.75678 6.80639L4.75842 6.80639ZM43.256 41.1936L4.75842 41.1936C4.1584 41.1936 3.576 41.7936 3.2864 42.6656C3.2864 43.5568 3.8864 44.4464 4.75842 44.7376L43.2384 44.7376C44.128 44.7376 44.7104 44.1376 44.7104 43.2656C44.728 42.08 44.1328 41.1936 43.256 41.1936Z" fill="#404040" ></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M24 48C10.7668 48 0 37.2332 0 24C0 10.7668 10.7668 0 24 0C37.2332 0 48 10.7668 48 24C48 37.2332 37.2332 48 24 48ZM24 3.69231C12.8031 3.69231 3.69231 12.8031 3.69231 24C3.69231 35.1969 12.8031 44.3077 24 44.3077C35.1969 44.3077 44.3077 35.1969 44.3077 24C44.3077 12.8031 35.1969 3.69231 24 3.69231Z" fill="#2C2C2C" ></path></svg>

After

Width:  |  Height:  |  Size: 477 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M40.1698 18.0929C40.1428 20.5301 39.3645 22.7586 38.3131 24.9092C36.7378 28.1319 34.6461 31.0118 32.3727 33.7664C30.2564 36.3308 27.9853 38.7519 25.5613 41.0284C24.7143 41.8237 23.9413 41.8481 23.073 41.0687C18.5767 37.0334 14.5359 32.6092 11.4515 27.3755C10.9495 26.5238 10.4945 25.6458 10.1025 24.7372C9.73107 23.8761 10.0514 22.9753 10.8534 22.6034C11.6577 22.2304 12.582 22.5576 12.9732 23.4086C14.8167 27.4176 17.5132 30.8241 20.4598 34.0548C21.596 35.3006 22.7863 36.4939 24.0184 37.6454C24.1708 37.7877 24.2667 37.9033 24.4831 37.6853C28.5664 33.5709 32.3892 29.2509 35.1723 24.1156C35.9836 22.6186 36.66 21.0543 36.8866 19.3498C37.2281 16.7802 36.712 14.3356 35.493 12.0807C33.1715 7.78618 29.5679 5.33714 24.6496 5.16445C19.0467 4.96786 14.0685 8.58606 12.3051 13.9294C11.8621 15.2717 11.6275 16.6474 11.6092 18.061C11.5962 19.0717 10.9335 19.7853 10.0201 19.783C9.10383 19.7808 8.43479 19.0505 8.43746 18.056C8.45818 10.5393 13.6357 4.02759 20.9324 2.34148C26.9693 0.946299 33.4364 3.45514 37.0904 8.60424C39.1088 11.4483 40.1115 14.6187 40.1698 18.0929ZM6.42075 43.1971C18.363 43.1971 30.2366 43.1971 42.1204 43.1971C42.1186 42.9919 41.9834 42.8579 41.8977 42.7076C40.3484 39.9866 38.7956 37.2673 37.2401 34.5498C36.9062 33.9668 36.8856 33.3861 37.257 32.8222C37.5915 32.314 38.0819 32.0681 38.6918 32.1099C39.2399 32.1477 39.6712 32.4169 39.9425 32.8899C42.0656 36.5932 44.1915 40.2948 46.2926 44.0105C46.9411 45.1576 46.1392 46.3818 44.7752 46.3821C37.6695 46.3845 30.564 46.3832 23.4585 46.3832C16.9643 46.3832 10.4699 46.3507 3.97625 46.4082C2.51885 46.4212 1.62531 45.0904 2.42647 43.7424C4.52293 40.2152 6.51577 36.6264 8.55216 33.0634C8.99072 32.2962 9.67627 31.9592 10.3942 32.1457C11.456 32.4219 11.9181 33.5491 11.3553 34.5444C10.2685 36.467 9.16594 38.3807 8.07099 40.2987C7.53258 41.2418 6.99643 42.1859 6.42075 43.1971ZM30.6461 17.8271C30.6465 21.3322 27.7864 24.1813 24.2754 24.1734C20.7946 24.1656 17.9554 21.3147 17.9555 17.8276C17.9557 14.3185 20.8087 11.4763 24.3244 11.4829C27.8051 11.4893 30.6458 14.3402 30.6461 17.8271ZM24.3201 14.6556C22.5583 14.656 21.1314 16.0714 21.1285 17.8213C21.1256 19.591 22.5741 21.0173 24.3575 21.0005C26.0699 20.9844 27.4807 19.5391 27.4733 17.8085C27.4659 16.0808 26.0403 14.6552 24.3201 14.6556Z" fill="#404040" ></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
clip-path: url(#clip-坡度);
}
.cls-2 {
fill: #fff;
}
</style>
<clipPath id="clip-坡度">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="坡度" class="cls-1">
<rect class="cls-2" width="32" height="32"/>
<path id="路径_15" data-name="路径 15" d="M202.1,188.337l2.629-2.191-8.447-3.106,1.533,8.871,2.629-2.194,9.341,11.209,1.656-1.379Zm-13.726-.435a1.075,1.075,0,0,0-1.07-.341,1.057,1.057,0,0,0-.5.277l-5.11,4.08a1.08,1.08,0,0,0-.406.84l-.007,17.386a1.079,1.079,0,0,0,1.077,1.077L205.7,211.2a1.078,1.078,0,0,0,.822-1.774Zm-4.934,21.164.007-15.788,3.968-3.171,15.974,18.941Z" transform="translate(-180.36 -181.131)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 876 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M12.1443 4L0 41.3958L5.12362 41.3958L8.08329 31.7577L21.6395 31.7577L24.5992 41.3958L29.7665 41.3958L17.6222 4L12.1443 4ZM9.53887 27.0451L14.7935 10.1281L14.9682 10.1281L20.1791 27.0451L9.53887 27.0451ZM40.2806 18.7995C37.9856 18.7995 36.1177 19.2656 34.7543 20.2898C33.1774 21.3946 32.173 23.1841 31.7752 25.5663L35.5451 25.9518C35.7586 24.7205 36.2972 23.8228 37.1608 23.2302C37.8789 22.7181 38.8493 22.4649 40.0332 22.4649C42.8327 22.4649 44.2301 23.9955 44.2301 27.0624L44.2301 27.9543L40.0671 28.0809C37.3403 28.1672 35.1861 28.8059 33.682 30.0833C32.0323 31.401 31.2075 33.3171 31.2075 35.7856C31.2075 37.6154 31.78 39.1057 32.9639 40.2565C34.041 41.4073 35.5451 42 37.4859 42C39.1356 42 40.5717 41.6145 41.7895 40.9355C42.8667 40.2968 43.7643 39.4049 44.4824 38.2944L44.4824 41.4016L48 41.4016L48 27.3501C48 24.6687 47.4275 22.626 46.3115 21.222C45.0161 19.6051 43.0074 18.7995 40.2806 18.7995ZM44.2252 31.2226L44.2252 32.5C44.2252 34.2032 43.6139 35.6475 42.4688 36.7983C41.3189 37.9491 39.9555 38.5418 38.3447 38.5418C37.3743 38.5418 36.5883 38.2426 36.0109 37.6902C35.3996 37.1378 35.1133 36.4588 35.1133 35.6072C35.1133 32.8855 36.8357 31.4355 40.3146 31.3492L44.2252 31.2226Z" fill="#2C2C2C" ></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M24.0001 48C22.6801 48 21.6001 46.9425 21.6001 45.65L21.6001 3.35C21.6001 2.0575 22.6801 1 24.0001 1C25.3201 1 26.4001 2.0575 26.4001 3.35L26.4001 45.65C26.4001 46.9425 25.3201 48 24.0001 48Z" fill="#2C2C2C" ></path><path d="M0 3.35C0 2.0575 1.08 1 2.4 1L45.6 1C46.92 1 48 2.0575 48 3.35C48 4.6425 46.92 5.7 45.6 5.7L2.4 5.7C1.08 5.7 0 4.6425 0 3.35Z" fill="#2C2C2C" ></path></svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
clip-path: url(#clip-标高);
}
.cls-2 {
fill: #fff;
}
</style>
<clipPath id="clip-标高">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="标高" class="cls-1">
<rect class="cls-2" width="32" height="32"/>
<path id="路径_8" data-name="路径 8" d="M84.131,193.119a1.056,1.056,0,0,1,1.116.982v7.857a1.056,1.056,0,0,1-1.116.982H54.367a1.056,1.056,0,0,1-1.116-.982V194.1a1.056,1.056,0,0,1,1.116-.982Zm-1.116,1.964H55.483v5.893H83.015Zm1.116-13.749a1.064,1.064,0,0,1,1.114.935,1.032,1.032,0,0,1-1.007,1.025l-.107,0H71.2l-7.858,6.914a1.227,1.227,0,0,1-1.578,0l-8.185-7.2-.018-.016-.032-.031.049.047a1.107,1.107,0,0,1-.092-.092l-.011-.014a.869.869,0,0,1-.182-.857l0-.008L53.31,182l.012-.029.02-.045.019-.035a1.1,1.1,0,0,1,.891-.552h.007q.053,0,.107,0ZM68.043,183.3H57.06l5.492,4.831Z" transform="translate(-53.247 -176.136)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M31.6062 5.51994L31.6062 5.56835L31.6062 5.39893C31.6062 5.44249 31.6062 5.48122 31.6062 5.52478C31.6062 5.51994 31.6062 5.5151 31.6062 5.51026C31.6062 5.51026 31.6062 5.5151 31.6062 5.51994Z" fill="#FFFFFF" ></path><path d="M31.6062 5.51994L31.6062 5.56835L31.6062 5.39893C31.6062 5.44249 31.6062 5.48122 31.6062 5.52478C31.6062 5.51994 31.6062 5.5151 31.6062 5.51026C31.6062 5.51026 31.6062 5.5151 31.6062 5.51994Z" fill="#000000" ></path><path d="M47.4452 11.6515L36.3485 0.55483C35.6088 -0.184944 34.4422 -0.184944 33.7024 0.55483L0.55483 33.7024C-0.184944 34.4422 -0.184944 35.6088 0.55483 36.3485L11.6515 47.4452C12.3912 48.1849 13.5578 48.1849 14.2976 47.4452L47.4452 14.2976C48.1849 13.5862 48.1849 12.3912 47.4452 11.6515ZM12.9887 43.4618L4.53823 35.0113L6.6722 32.8773L8.1233 34.3284C8.86307 35.0682 10.0296 35.0682 10.7694 34.3284C11.5092 33.5886 11.5092 32.4221 10.7694 31.6823L9.31832 30.2312L11.4523 28.0972L12.9034 29.5483C13.6432 30.2881 14.8097 30.2881 15.5495 29.5483C16.2893 28.8085 16.2893 27.642 15.5495 26.9022L14.0984 25.4511L16.2324 23.3171L17.6835 24.7682C18.4232 25.508 19.5898 25.508 20.3296 24.7682C21.0694 24.0285 21.0694 22.8619 20.3296 22.1221L18.8785 20.671L21.0124 18.537L22.4635 19.9881C23.2033 20.7279 24.3699 20.7279 25.1097 19.9881C25.8494 19.2484 25.8494 18.0818 25.1097 17.342L23.6586 15.8909L25.7925 13.757L27.2436 15.2081C27.9834 15.9478 29.15 15.9478 29.8897 15.2081C30.6295 14.4683 30.6295 13.3017 29.8897 12.5619L28.4386 11.1108L30.5726 8.97688L32.0237 10.428C32.7635 11.1678 33.9301 11.1678 34.6698 10.428C35.4096 9.6882 35.4096 8.52164 34.6698 7.78186L33.2187 6.33076L35.0682 4.48133L43.5187 12.9318L12.9887 43.4618Z" fill="#2C2C2C" ></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M2.77325 47L32.7244 18.0388L29.9511 15.3572L0 44.3184L2.77325 47Z" fill="#2C2C2C" ></path><path d="M41.3441 25.7529L47.9999 0.999853L22.1448 7.18811L41.3441 25.7529Z" fill="#2C2C2C" ></path></svg>

After

Width:  |  Height:  |  Size: 347 B

1
demo/assets/icon/线.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M0 7L48 7L48 10.3L0 10.3L0 7ZM0 16.9L48 16.9L48 23.5L0 23.5L0 16.9ZM0 30.1L48 30.1L48 40L0 40L0 30.1Z" fill="#2C2C2C" ></path></svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
clip-path: url(#clip-角度);
}
.cls-2 {
fill: #fff;
}
</style>
<clipPath id="clip-角度">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="角度" class="cls-1">
<rect class="cls-2" width="32" height="32"/>
<path id="路径_7" data-name="路径 7" d="M39.587,50.766h13.7a1,1,0,0,1,0,2H23.171a1,1,0,0,1,0-2h1.418l6.582-7.006v-.006a.517.517,0,0,1,.14-.357.456.456,0,0,1,.337-.144l12.1-12.876a.451.451,0,0,1,.665,0,.524.524,0,0,1,0,.708L32.883,43.355a8.3,8.3,0,0,1,6.7,7.411Zm-.949,0a7.254,7.254,0,0,0-6.611-6.5l-6.108,6.5Z" transform="translate(-22.229 -26.489)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 816 B

View File

@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
fill: none;
}
.cls-2 {
clip-path: url(#clip-距离);
}
.cls-3 {
clip-path: url(#clip-path);
}
.cls-4 {
fill: #fff;
}
</style>
<clipPath id="clip-path">
<rect id="矩形_1" data-name="矩形 1" class="cls-1" width="32" height="23.606" transform="translate(0 0)"/>
</clipPath>
<clipPath id="clip-距离">
<rect width="32" height="32"/>
</clipPath>
</defs>
<g id="距离" class="cls-2">
<rect class="cls-4" width="32" height="32"/>
<g id="组_2" data-name="组 2" transform="translate(0 4.197)">
<g id="组_1" data-name="组 1" class="cls-3">
<path id="路径_1" data-name="路径 1" d="M29.692,3.03,27.55.919a.529.529,0,0,1-.014-.756A.549.549,0,0,1,28.3.15l.014.013,3.067,3.023a.529.529,0,0,1,0,.756L28.317,6.966a.549.549,0,0,1-.767.013.529.529,0,0,1-.014-.756l.014-.013L29.692,4.1H2.31L4.452,6.21a.528.528,0,0,1,.013.756.547.547,0,0,1-.766.013l-.014-.013L.616,3.942a.531.531,0,0,1,0-.756L3.685.163a.548.548,0,0,1,.767.014.528.528,0,0,1,0,.742L2.31,3.03ZM24.136,15.055H23.051V18H21.966v-2.94H20.882V18H19.8v-2.94H18.712V18H17.627v-2.94H16.543v5.078H15.458V15.055H14.373V18H13.288v-2.94H12.2V18H11.119v-2.94H10.034V18H8.949v-2.94H7.865V18H6.78v-2.94H5.7v5.078H4.61V15.055H1.9a.27.27,0,0,0-.272.268v6.413A.269.269,0,0,0,1.9,22H30.1a.268.268,0,0,0,.271-.267V15.323a.269.269,0,0,0-.271-.268H27.39v5.078H26.305V15.055H25.221V18H24.136Zm5.966-1.6A1.884,1.884,0,0,1,32,15.323v6.413a1.885,1.885,0,0,1-1.9,1.871H1.9A1.885,1.885,0,0,1,0,21.736V15.323a1.885,1.885,0,0,1,1.9-1.871Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M17.13 45.7359L21.708 45.7359L21.708 48L17.13 48L17.13 45.7359ZM7.97408 45.7359L12.5521 45.7359L12.5521 48L7.97408 48L7.97408 45.7359ZM2.26408 44.6039L0 44.6039L0 48L3.39615 48L3.39615 45.7359L2.26408 45.7359L2.26408 44.6039ZM0 26.2859L2.26408 26.2859L2.26408 30.8638L0 30.8638L0 26.2859ZM0 7.97408L2.26408 7.97408L2.26408 12.5521L0 12.5521L0 7.97408ZM0 35.448L2.26408 35.448L2.26408 40.0259L0 40.0259L0 35.448ZM0 17.13L2.26408 17.13L2.26408 21.708L0 21.708L0 17.13ZM0 3.39615L2.26408 3.39615L2.26408 2.26408L3.39615 2.26408L3.39615 0L0 0L0 3.39615ZM26.2921 0L30.87 0L30.87 2.26408L26.2921 2.26408L26.2921 0ZM35.448 0L40.0259 0L40.0259 2.26408L35.448 2.26408L35.448 0ZM17.13 0L21.708 0L21.708 2.26408L17.13 2.26408L17.13 0ZM7.97408 0L12.5521 0L12.5521 2.26408L7.97408 2.26408L7.97408 0ZM44.6039 0L44.6039 2.26408L45.7359 2.26408L45.7359 3.39615L48 3.39615L48 0L44.6039 0ZM45.7359 17.13L48 17.13L48 21.708L45.7359 21.708L45.7359 17.13ZM45.7359 7.97408L48 7.97408L48 12.5521L45.7359 12.5521L45.7359 7.97408ZM36.4432 31.2308L40.2747 28.2638C40.5609 28.0399 40.7163 27.6916 40.6853 27.3309C40.6542 26.9701 40.4427 26.6466 40.1255 26.4787L20.7376 16.0415C20.3644 15.8424 19.9104 15.8798 19.5745 16.1348C19.2386 16.396 19.0893 16.8252 19.1888 17.2357L24.4323 38.6202C24.5194 38.9748 24.7744 39.2547 25.1165 39.3728C25.4586 39.491 25.838 39.4288 26.1242 39.2111L29.9557 36.2441L38.2408 46.9426C38.4522 47.2163 38.7695 47.3593 39.0867 47.3593C39.3168 47.3593 39.547 47.2847 39.7398 47.1354L44.5292 43.4283C44.7531 43.2541 44.9024 42.9991 44.9336 42.7192C44.9708 42.4393 44.89 42.1532 44.7158 41.9293L36.4432 31.2308Z" fill="#2C2C2C" ></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48" fill="none"><path d="M28.7725 16.8582L16.6882 28.9801C16.0335 29.6368 16.0335 30.7313 16.6882 31.3881C17.0156 31.7164 17.452 31.8806 17.8885 31.8806C18.3249 31.8806 18.7614 31.7164 19.0887 31.3881L31.173 19.2662C31.8277 18.6094 31.8277 17.5149 31.173 16.8582C30.5183 16.2015 29.4272 16.2015 28.7725 16.8582Z" fill="#404040" ></path><path d="M44.5667 11.9055C45.385 11.9055 46.0397 11.2488 46.0397 10.4552L46.0397 3.47761C46.0397 2.65672 45.385 2 44.5667 2L37.6107 2C36.7924 2 36.1377 2.65672 36.1377 3.47761L36.1377 5.06468L11.9418 5.06468L11.9418 3.47761C11.9418 2.65672 11.2871 2 10.4688 2L3.51277 2C2.69442 2 2.03974 2.65672 2.03974 3.47761L2.03974 10.4279C2.03974 11.2488 2.69442 11.9055 3.51277 11.9055L5.20403 11.9055L5.20403 36.0671L3.51277 36.0671C2.69442 36.0671 2.03974 36.7239 2.03974 37.5448L2.03974 44.5224C2.03974 45.3433 2.69442 46 3.51277 46L10.4688 46C11.2598 46 11.9145 45.3433 11.9418 44.5224L11.9418 42.7438L36.1377 42.7438L36.1377 44.5224C36.1377 45.3433 36.7924 46 37.6107 46L44.5667 46C45.385 46 46.0397 45.3433 46.0397 44.5224L46.0397 37.5448C46.0397 36.7239 45.385 36.0671 44.5667 36.0671L42.7936 36.0671L42.7936 11.9055L44.5667 11.9055ZM39.0837 4.92786L43.0937 4.92786L43.0937 8.95024L39.0837 8.95024L39.0837 4.92786ZM4.9858 4.92786L8.99572 4.92786L8.99572 8.95024L4.9858 8.95024L4.9858 4.92786ZM8.99572 43.0448L4.9858 43.0448L4.9858 39.0224L8.99572 39.0224L8.99572 43.0448ZM43.0937 43.0448L39.0837 43.0448L39.0837 39.0224L43.0937 39.0224L43.0937 43.0448ZM39.3838 36.0671L37.6107 36.0671C36.7924 36.0671 36.1377 36.7239 36.1377 37.5448L36.1377 39.3234L11.9418 39.3234L11.9418 37.5448C11.9418 36.7239 11.2871 36.0671 10.4688 36.0671L8.61382 36.0671L8.61382 11.9055L10.4688 11.9055C11.2598 11.9055 11.9145 11.2488 11.9418 10.4552L11.9418 8.48507L36.1377 8.48507L36.1377 10.4279C36.1377 11.2488 36.7924 11.9055 37.6107 11.9055L39.3838 11.9055L39.3838 36.0671Z" fill="#404040" ></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -344,8 +344,235 @@
let unsubscribePresetSaved = null; let unsubscribePresetSaved = null;
let unsubscribePresetChanged = null; let unsubscribePresetChanged = null;
let unsubscribePresetDeleted = null; let unsubscribePresetDeleted = null;
let unsubscribePinCreated = null;
let unsubscribePinDeleted = null;
let unsubscribePinChanged = null;
const PIN_CACHE_KEY = 'iflow-demo-pins-v1';
let pinCache = [];
// 主视图缓存相关
let unsubscribeMainViewSaved = null;
let unsubscribeMainViewRestored = null;
let unsubscribeModelLoaded = null;
const MAIN_VIEW_CACHE_KEY = 'iflow-demo-main-view-v1';
function loadMainViewCache() {
try {
const raw = localStorage.getItem(MAIN_VIEW_CACHE_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw);
return typeof parsed === 'object' && parsed !== null ? parsed : {};
} catch (error) {
console.warn('读取主视图缓存失败:', error);
return {};
}
}
function saveMainViewCache(cache) {
localStorage.setItem(MAIN_VIEW_CACHE_KEY, JSON.stringify(cache));
}
function saveMainViewToCache(url, viewData) {
const cache = loadMainViewCache();
cache[url] = {
viewData: viewData,
timestamp: Date.now()
};
saveMainViewCache(cache);
console.log('💾 主视图已缓存:', url);
}
function applyMainViewIfExists(url) {
const cache = loadMainViewCache();
const cached = cache[url];
if (cached && cached.viewData) {
if (engine && engine.engine) {
engine.engine.setMainViewPort(cached.viewData);
console.log('✅ 已应用缓存的主视图:', url);
}
}
}
function clearMainViewCache(url) {
const cache = loadMainViewCache();
if (cache[url]) {
delete cache[url];
saveMainViewCache(cache);
console.log('🗑️ 主视图缓存已清除:', url);
}
}
function bindMainViewEvents() {
if (!engine || typeof engine.on !== 'function') return;
if (unsubscribeMainViewSaved) {
unsubscribeMainViewSaved();
unsubscribeMainViewSaved = null;
}
if (unsubscribeModelLoaded) {
unsubscribeModelLoaded();
unsubscribeModelLoaded = null;
}
// 监听主视图保存事件
unsubscribeMainViewSaved = engine.on('view:main-view-saved', (data) => {
console.log('[Demo] view:main-view-saved:', data);
saveMainViewToCache(current3dModelUrl, data.viewData);
});
// 监听主视图重置事件
unsubscribeMainViewRestored = engine.on('view:main-view-restored', () => {
console.log('[Demo] view:main-view-restored');
clearMainViewCache(current3dModelUrl);
});
// 监听模型加载完成事件
unsubscribeModelLoaded = engine.on('engine:model-loading-completed', () => {
console.log('[Demo] engine:model-loading-completed, applying main view if exists');
applyMainViewIfExists(current3dModelUrl);
});
console.log('✅ 主视图事件绑定完成');
}
function loadPinCache() {
try {
const raw = localStorage.getItem(PIN_CACHE_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
console.warn('读取图钉缓存失败,已忽略:', error);
return [];
}
}
function savePinCache(list) {
localStorage.setItem(PIN_CACHE_KEY, JSON.stringify(list));
}
function normalizeParentId(parentId) {
return parentId == null || parentId === '' ? null : parentId;
}
function sortPinRecords(list) {
return [...list].sort((a, b) => {
const parentA = normalizeParentId(a.parentId) || '';
const parentB = normalizeParentId(b.parentId) || '';
if (parentA !== parentB) return parentA.localeCompare(parentB);
return (a.seq || 0) - (b.seq || 0);
});
}
function resequencePinRecords(list, parentId) {
const normalizedParentId = normalizeParentId(parentId);
const siblings = sortPinRecords(list.filter((item) => normalizeParentId(item.parentId) === normalizedParentId));
siblings.forEach((item, index) => {
item.seq = index;
});
}
function collectDescendantIds(list, parentId) {
const ids = [];
const stack = [parentId];
while (stack.length > 0) {
const currentId = stack.pop();
list.forEach((item) => {
if (normalizeParentId(item.parentId) === currentId) {
ids.push(item.id);
stack.push(item.id);
}
});
}
return ids;
}
function pushPinRecordsToEngine() {
if (!engine || !engine.engine) return;
if (typeof engine.engine.setPinRecords === 'function') {
engine.engine.setPinRecords(pinCache);
return;
}
if (typeof engine.engine.setPinList === 'function') {
engine.engine.setPinList(pinCache);
}
}
function injectPinsFromCache() {
pinCache = loadPinCache();
if (pinCache.length > 0) {
pushPinRecordsToEngine();
console.log('✅ 已注入缓存图钉列表,数量:', pinCache.length);
}
}
function bindPinEvents() {
if (!engine || typeof engine.on !== 'function') return;
if (unsubscribePinCreated) {
unsubscribePinCreated();
unsubscribePinCreated = null;
}
if (unsubscribePinDeleted) {
unsubscribePinDeleted();
unsubscribePinDeleted = null;
}
if (unsubscribePinChanged) {
unsubscribePinChanged();
unsubscribePinChanged = null;
}
unsubscribePinCreated = engine.on('drawingPin:create', ({ record }) => {
console.log('[Demo] drawingPin:create:', record);
const normalizedRecord = {
...record,
parentId: normalizeParentId(record.parentId)
};
pinCache.push(normalizedRecord);
resequencePinRecords(pinCache, normalizedRecord.parentId);
savePinCache(pinCache);
pushPinRecordsToEngine();
});
unsubscribePinDeleted = engine.on('drawingPin:delete', ({ id }) => {
console.log('[Demo] drawingPin:delete:', id);
const record = pinCache.find((item) => item.id === id);
if (!record) return;
const targetParentId = normalizeParentId(record.parentId);
const descendantIds = collectDescendantIds(pinCache, id);
const deleteIds = new Set([id, ...descendantIds]);
pinCache = pinCache.filter((item) => !deleteIds.has(item.id));
resequencePinRecords(pinCache, targetParentId);
savePinCache(pinCache);
pushPinRecordsToEngine();
});
unsubscribePinChanged = engine.on('drawingPin:update', ({ id, patch }) => {
console.log('[Demo] drawingPin:update:', id, patch);
const index = pinCache.findIndex((item) => item.id === id);
if (index !== -1) {
const previousParentId = normalizeParentId(pinCache[index].parentId);
const nextParentId = Object.prototype.hasOwnProperty.call(patch, 'parentId')
? normalizeParentId(patch.parentId)
: previousParentId;
pinCache[index] = {
...pinCache[index],
...patch,
parentId: nextParentId,
};
resequencePinRecords(pinCache, previousParentId);
resequencePinRecords(pinCache, nextParentId);
}
savePinCache(pinCache);
pushPinRecordsToEngine();
});
console.log('✅ 图钉事件绑定完成');
}
// const DEFAULT_3D_MODEL_URL = 'https://pub-8092fd0db2e14822a9f742ad0050750c.r2.dev/e9603d6b-c885-4f1b-84b0-2589bc9dc44f'; // const DEFAULT_3D_MODEL_URL = 'https://pub-8092fd0db2e14822a9f742ad0050750c.r2.dev/e9603d6b-c885-4f1b-84b0-2589bc9dc44f';
const DEFAULT_3D_MODEL_URL = 'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/417664a3-76c8-4d94-9344-1337246a5d4e'; // const DEFAULT_3D_MODEL_URL = 'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/417664a3-76c8-4d94-9344-1337246a5d4e';
//const DEFAULT_3D_MODEL_URL = 'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/cc389dc8-c6d5-43db-81f1-6998d4eee8f6';
const DEFAULT_3D_MODEL_URL = 'https://pub-8092fd0db2e14822a9f742ad0050750c.r2.dev/bc7de1ae-a47e-458f-b3da-2a6d6b8f39af';
//const DEFAULT_3D_MODEL_URL = 'https://pub-8092fd0db2e14822a9f742ad0050750c.r2.dev/66ad9a66-5ca8-47ac-9139-6aa8756069c1/'; //const DEFAULT_3D_MODEL_URL = 'https://pub-8092fd0db2e14822a9f742ad0050750c.r2.dev/66ad9a66-5ca8-47ac-9139-6aa8756069c1/';
let current3dModelUrl = DEFAULT_3D_MODEL_URL; let current3dModelUrl = DEFAULT_3D_MODEL_URL;
@@ -457,6 +684,30 @@
unsubscribePresetDeleted(); unsubscribePresetDeleted();
unsubscribePresetDeleted = null; unsubscribePresetDeleted = null;
} }
if (unsubscribePinCreated) {
unsubscribePinCreated();
unsubscribePinCreated = null;
}
if (unsubscribePinDeleted) {
unsubscribePinDeleted();
unsubscribePinDeleted = null;
}
if (unsubscribePinChanged) {
unsubscribePinChanged();
unsubscribePinChanged = null;
}
if (unsubscribeMainViewSaved) {
unsubscribeMainViewSaved();
unsubscribeMainViewSaved = null;
}
if (unsubscribeMainViewRestored) {
unsubscribeMainViewRestored();
unsubscribeMainViewRestored = null;
}
if (unsubscribeModelLoaded) {
unsubscribeModelLoaded();
unsubscribeModelLoaded = null;
}
if (engine) { engine.destroy(); engine = null; } if (engine) { engine.destroy(); engine = null; }
if (engine2d) { engine2d.destroy(); engine2d = null; } if (engine2d) { engine2d.destroy(); engine2d = null; }
if (engine720) { engine720.destroy(); engine720 = null; } if (engine720) { engine720.destroy(); engine720 = null; }
@@ -626,6 +877,9 @@
if (success) { if (success) {
injectSettingPresetsFromCache(); injectSettingPresetsFromCache();
bindPresetEvents(); bindPresetEvents();
bindPinEvents();
bindMainViewEvents();
injectPinsFromCache();
updateEngineStatus('已初始化'); updateEngineStatus('已初始化');
console.log('✅ 3D 引擎初始化成功'); console.log('✅ 3D 引擎初始化成功');
loadModel(); loadModel();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -206,6 +206,26 @@ ComponentDetailManager
--- ---
## 9) 保存主视图
文件:`src/components/engine/index.ts`
```text
点击"将当前视图设为主视图"
-> Engine.saveMainView()
-> rawEngine.viewCube.saveMainViewPort() -> 返回 viewData
-> registry.emit('view:main-view-saved', { viewData })
```
外部订阅:
```text
bimEngine.on('view:main-view-saved', handler)
-> ManagerRegistry.on('view:main-view-saved', handler)
```
---
## 9) 注意事项 ## 9) 注意事项
- 旧写法 `registry.engine3d?.xxx()`(大量透传)已不再推荐 - 旧写法 `registry.engine3d?.xxx()`(大量透传)已不再推荐

View File

@@ -216,6 +216,82 @@ bimEngine.on('encoding:error', (data) => {
// 启动编码 // 启动编码
engineComp?.startOneClickEncoding(); engineComp?.startOneClickEncoding();
// 需要时取消订阅
// unsubStart();
// unsubComplete();
// unsubError();
```
---
## 10) 视图控制(主视图保存/恢复)
### 所属模块
- 调用入口:`bimEngine.engine?.getEngineComponent()`
- 事件总线:`bimEngine`(通过 `ManagerRegistry` 桥接)
- 源码位置:`src/components/engine/index.ts`
### 方法签名
```ts
// 保存主视图
saveMainView(): any
// 恢复主视图(重置)
restoreMainView(): void
// 设置主视图(传入缓存数据)
setMainViewPort(viewData: any): void
```
### 事件列表
| 事件名 | 触发时机 | payload |
|--------|---------|---------|
| `view:main-view-saved` | 用户点击"将当前视图设为主视图"后 | `{ viewData: any }` |
| `view:main-view-restored` | 用户点击"重置主视图"后 | `{}` |
### 行为约定
- `saveMainView()`:调用底层 `viewCube.saveMainViewPort()` 获取视点数据,并触发 `view:main-view-saved` 事件。
- `restoreMainView()`:调用底层 `viewCube.resetMainViewPort()` 恢复默认主视图,并触发 `view:main-view-restored` 事件。
- `setMainViewPort(viewData)`:调用底层 `viewCube.setMainViewPort(viewData)` 恢复指定视点,**不触发事件**(用于程序初始化时恢复缓存)。
- 所有事件经 `Engine` 组件桥接后冒泡到 `bimEngine` 事件总线。
- 订阅方式与 SDK 其他事件一致:使用 `bimEngine.on(event, handler)`,返回的函数可用于取消订阅。
### 调用示例
```ts
const engineComp = bimEngine.engine?.getEngineComponent();
// 1. 保存主视图(用户操作触发)
engineComp?.saveMainView();
// 2. 监听保存事件,持久化到 localStorage
const unsubSaved = bimEngine.on('view:main-view-saved', ({ viewData }) => {
console.log('主视图已保存:', viewData);
localStorage.setItem('mainView', JSON.stringify(viewData));
});
// 3. 监听重置事件,清理缓存
const unsubRestored = bimEngine.on('view:main-view-restored', () => {
console.log('主视图已重置');
localStorage.removeItem('mainView');
});
// 4. 模型加载完成后,自动恢复缓存的主视图
bimEngine.on('engine:model-loading-completed', () => {
const cached = localStorage.getItem('mainView');
if (cached) {
engineComp?.setMainViewPort(JSON.parse(cached));
}
});
// 需要时取消订阅
// unsubSaved();
// unsubRestored();
``` ```
--- ---
@@ -332,3 +408,212 @@ bimEngine.engine?.getEngineComponent()?.startOneClickEncoding();
// unsubComplete(); // unsubComplete();
// unsubError(); // unsubError();
``` ```
---
## 11) 图钉DrawingPin管理
### 所属模块
- 调用入口:`bimEngine.engine?.getEngineComponent()` / `bimEngine.emit()` / `bimEngine.on()`
- 源码位置:`src/components/engine/index.ts``src/components/component-tree-drawer/pin-tab.ts`
- 初始化要求:需先调用 `bimEngine.initConstructTreeBtn()` 初始化构件树面板
### 概述
图钉功能用于保存和展示 3D 视图中的相机视角,支持文件夹分组管理。**外部系统完全掌控图钉的创建、编辑、删除和持久化**SDK 仅负责渲染展示。
对接方式:
- **批量传入配置**`setPinRecords()` 一次性设置完整图钉列表
- **增删改**:通过 `bimEngine.emit()` 发送 `drawingPin:create|update|delete` 事件
- **监听变更**:订阅 `drawingPin:list-updated` 获取用户操作后的最新列表
### 数据类型
```ts
interface DrawingPinRecord {
id: string; // 唯一标识(外部生成)
parentId?: string | null; // 父文件夹 IDnull/undefined 表示根节点
name: string; // 显示名称
type: 'pin' | 'folder'; // 类型:图钉 或 文件夹
seq: number; // 排序序号(同级内排序)
data?: any; // 视角数据(仅 type='pin' 时有值,由外部系统生成)
}
```
### API 列表
#### A. 批量传入图钉配置
```ts
bimEngine.engine?.getEngineComponent()?.setPinRecords(
records: DrawingPinRecord[]
): void
```
- **作用**外部一次性传入完整图钉列表平铺结构SDK 内部自动构建树形层级。
- **场景**:初始化时从后端/缓存加载已有图钉配置。
- **注意**:会触发 `drawingPin:list-updated` 事件。
#### B. 创建 / 更新 / 删除(事件接口)
外部通过 `bimEngine.emit()` 向 SDK 发送操作指令:
| 事件 | payload | 说明 |
|------|---------|------|
| `drawingPin:create` | `{ record: DrawingPinRecord }` | 创建图钉或文件夹 |
| `drawingPin:update` | `{ id: string; patch: Partial<Pick<DrawingPinRecord, 'name' \| 'parentId' \| 'seq' \| 'data'>> }` | 编辑图钉/文件夹 |
| `drawingPin:delete` | `{ id: string }` | 删除图钉或文件夹 |
#### C. 监听列表变更
| 事件 | 方向 | payload | 说明 |
|------|------|---------|------|
| `drawingPin:list-updated` | SDK → 外部 | `{ records: DrawingPinRecord[] }` | 列表发生任何变更时触发(含用户界面操作) |
### 调用示例
#### 1. 初始化时批量传入图钉配置
```ts
const engineComp = bimEngine.engine?.getEngineComponent();
// 从后端获取已有配置
const pinRecords: DrawingPinRecord[] = [
{
id: 'folder_001',
parentId: null,
name: '一层图钉',
type: 'folder',
seq: 0,
},
{
id: 'pin_001',
parentId: 'folder_001',
name: '入口视角',
type: 'pin',
seq: 0,
data: { /* 外部系统保存的视角数据 */ },
},
{
id: 'pin_002',
parentId: 'folder_001',
name: '大厅视角',
type: 'pin',
seq: 1,
data: { /* 外部系统保存的视角数据 */ },
},
];
// 一次性传入
engineComp?.setPinRecords(pinRecords);
```
#### 2. 创建图钉(外部生成视角数据)
```ts
// 外部系统自行生成视角数据
const viewData = { /* 外部系统生成的视角数据 */ };
const record: DrawingPinRecord = {
id: `pin_${Date.now()}`,
parentId: null, // 或指定文件夹 ID实现分组
name: '新图钉',
type: 'pin',
seq: 0,
data: viewData,
};
bimEngine.emit('drawingPin:create', { record });
```
#### 3. 创建文件夹
```ts
const record: DrawingPinRecord = {
id: `folder_${Date.now()}`,
parentId: null, // 或指定父文件夹 ID实现嵌套
name: '新文件夹',
type: 'folder',
seq: 0,
};
bimEngine.emit('drawingPin:create', { record });
```
#### 4. 编辑(重命名 / 移动分组 / 调序)
```ts
// 重命名
bimEngine.emit('drawingPin:update', {
id: 'pin_001',
patch: { name: '入口视角(已更新)' },
});
// 移动到另一个文件夹
bimEngine.emit('drawingPin:update', {
id: 'pin_001',
patch: { parentId: 'folder_002' },
});
// 调整排序
bimEngine.emit('drawingPin:update', {
id: 'pin_001',
patch: { seq: 2 },
});
```
#### 5. 删除
```ts
bimEngine.emit('drawingPin:delete', { id: 'pin_001' });
```
#### 6. 监听变更并持久化
```ts
bimEngine.on('drawingPin:list-updated', ({ records }) => {
console.log('图钉列表已更新:', records);
// 同步到后端或 localStorage
localStorage.setItem('drawingPins', JSON.stringify(records));
});
```
#### 7. 完整对接示例(初始化 + 双向同步)
```ts
const engineComp = bimEngine.engine?.getEngineComponent();
// 1. 监听变更,持久化到后端
bimEngine.on('drawingPin:list-updated', ({ records }) => {
fetch('/api/save-pins', {
method: 'POST',
body: JSON.stringify(records),
});
});
// 2. 模型加载完成后,从后端恢复图钉配置
bimEngine.on('engine:model-loading-completed', () => {
fetch('/api/get-pins')
.then((res) => res.json())
.then((records: DrawingPinRecord[]) => {
engineComp?.setPinRecords(records);
});
});
// 3. 业务层提供"添加图钉"按钮
function onAddPinClick() {
// 外部系统自行生成视角数据
const viewData = { /* 外部系统生成的视角数据 */ };
const record: DrawingPinRecord = {
id: `pin_${Date.now()}`,
parentId: null,
name: '新视角',
type: 'pin',
seq: 0,
data: viewData,
};
bimEngine.emit('drawingPin:create', { record });
}
```

12
package-lock.json generated
View File

@@ -1,15 +1,15 @@
{ {
"name": "iflow-engine", "name": "iflow-engine",
"version": "2.7.1", "version": "3.6.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "iflow-engine", "name": "iflow-engine",
"version": "2.7.1", "version": "3.6.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"iflow-engine-base": "3.5.31", "iflow-engine-base": "^3.7.6",
"three": "^0.182.0" "three": "^0.182.0"
}, },
"devDependencies": { "devDependencies": {
@@ -2257,9 +2257,9 @@
"optional": true "optional": true
}, },
"node_modules/iflow-engine-base": { "node_modules/iflow-engine-base": {
"version": "3.5.31", "version": "3.7.6",
"resolved": "https://registry.npmjs.org/iflow-engine-base/-/iflow-engine-base-3.5.31.tgz", "resolved": "https://registry.npmjs.org/iflow-engine-base/-/iflow-engine-base-3.7.6.tgz",
"integrity": "sha512-HRHvAkM3nJDviTDKOoB+5u+w+w73PpnjPZLdU3h6CpGfHRJua4r9rBWAIq3aABb6jwMFQAJL083Hu/XW3K9fDQ==", "integrity": "sha512-4/QJFbIUlXfNYdlMBPbRivdhzNryUfR2OAn/wFa/ZHFnItRGzrlSaYD4+qdKtKkUrlBZPTeCmJyjf4cRMQe+KA==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/three": "^0.181.0", "@types/three": "^0.181.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "iflow-engine", "name": "iflow-engine",
"version": "2.6.1", "version": "3.7.0",
"description": "iFlow Engine SDK for Vue2, Vue3, React and HTML", "description": "iFlow Engine SDK for Vue2, Vue3, React and HTML",
"main": "./dist/iflow-engine.umd.js", "main": "./dist/iflow-engine.umd.js",
"module": "./dist/iflow-engine.es.js", "module": "./dist/iflow-engine.es.js",
@@ -59,7 +59,7 @@
"vite-plugin-dts": "^4.5.4" "vite-plugin-dts": "^4.5.4"
}, },
"dependencies": { "dependencies": {
"iflow-engine-base": "^3.5.0", "iflow-engine-base": "^3.7.6",
"three": "^0.182.0" "three": "^0.182.0"
} }
} }

View File

@@ -9,12 +9,12 @@ import { BottomDockManager } from './managers/bottom-dock-manager';
import { MeasureDockManager } from './managers/measure-dock-manager'; import { MeasureDockManager } from './managers/measure-dock-manager';
import { SectionDockManager } from './managers/section-dock-manager'; import { SectionDockManager } from './managers/section-dock-manager';
import { WalkDockManager } from './managers/walk-dock-manager'; import { WalkDockManager } from './managers/walk-dock-manager';
import { ComponentTreeDrawerManager } from './managers/component-tree-drawer-manager';
import { MeasureDialogManager } from './managers/measure-dialog-manager'; import { MeasureDialogManager } from './managers/measure-dialog-manager';
import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manager'; import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manager';
import { SectionAxisDialogManager } from './managers/section-axis-dialog-manager'; import { SectionAxisDialogManager } from './managers/section-axis-dialog-manager';
import { SectionBoxDialogManager } from './managers/section-box-dialog-manager'; import { SectionBoxDialogManager } from './managers/section-box-dialog-manager';
import { WalkControlManager } from './managers/walk-control-manager';
import { EngineInfoDialogManager } from './managers/engine-info-dialog-manager'; import { EngineInfoDialogManager } from './managers/engine-info-dialog-manager';
import { SettingDialogManager } from './managers/setting-dialog-manager'; import { SettingDialogManager } from './managers/setting-dialog-manager';
import { ComponentDetailManager } from './managers/component-detail-manager'; import { ComponentDetailManager } from './managers/component-detail-manager';
@@ -47,12 +47,12 @@ export class BimEngine {
public measureDock: MeasureDockManager | null = null; public measureDock: MeasureDockManager | null = null;
public sectionDock: SectionDockManager | null = null; public sectionDock: SectionDockManager | null = null;
public walkDock: WalkDockManager | null = null; public walkDock: WalkDockManager | null = null;
public componentTreeDrawer: ComponentTreeDrawerManager | null = null;
public measure: MeasureDialogManager | null = null; public measure: MeasureDialogManager | null = null;
public sectionPlane: SectionPlaneDialogManager | null = null; public sectionPlane: SectionPlaneDialogManager | null = null;
public sectionAxis: SectionAxisDialogManager | null = null; public sectionAxis: SectionAxisDialogManager | null = null;
public sectionBox: SectionBoxDialogManager | null = null; public sectionBox: SectionBoxDialogManager | null = null;
public walkControl: WalkControlManager | null = null;
public engineInfo: EngineInfoDialogManager | null = null; public engineInfo: EngineInfoDialogManager | null = null;
public componentDetail: ComponentDetailManager | null = null; public componentDetail: ComponentDetailManager | null = null;
public aiChat: AiChatManager | null = null; public aiChat: AiChatManager | null = null;
@@ -156,14 +156,15 @@ export class BimEngine {
this.registry.walkDock = this.walkDock; this.registry.walkDock = this.walkDock;
this.walkDock.init(); this.walkDock.init();
this.componentTreeDrawer = new ComponentTreeDrawerManager(this.registry);
this.registry.componentTreeDrawer = this.componentTreeDrawer;
this.radialToolbar = new RadialToolbarManager(this.wrapper, this.registry); this.radialToolbar = new RadialToolbarManager(this.wrapper, this.registry);
this.measure = new MeasureDialogManager(this.registry); this.measure = new MeasureDialogManager(this.registry);
this.sectionPlane = new SectionPlaneDialogManager(this.registry); this.sectionPlane = new SectionPlaneDialogManager(this.registry);
this.sectionAxis = new SectionAxisDialogManager(this.registry); this.sectionAxis = new SectionAxisDialogManager(this.registry);
this.sectionBox = new SectionBoxDialogManager(this.registry); this.sectionBox = new SectionBoxDialogManager(this.registry);
this.walkControl = new WalkControlManager(this.registry);
this.walkControl.init();
this.engineInfo = new EngineInfoDialogManager(this.registry); this.engineInfo = new EngineInfoDialogManager(this.registry);
this.engineInfo.init(); this.engineInfo.init();
@@ -176,7 +177,6 @@ export class BimEngine {
this.registry.sectionPlane = this.sectionPlane; this.registry.sectionPlane = this.sectionPlane;
this.registry.sectionAxis = this.sectionAxis; this.registry.sectionAxis = this.sectionAxis;
this.registry.sectionBox = this.sectionBox; this.registry.sectionBox = this.sectionBox;
this.registry.walkControl = this.walkControl;
this.registry.engineInfo = this.engineInfo; this.registry.engineInfo = this.engineInfo;
@@ -261,6 +261,7 @@ export class BimEngine {
this.measureDock?.destroy(); this.measureDock?.destroy();
this.sectionDock?.destroy(); this.sectionDock?.destroy();
this.walkDock?.destroy(); this.walkDock?.destroy();
this.componentTreeDrawer?.destroy();
this.bottomDock?.destroy(); this.bottomDock?.destroy();
this.engine?.destroy(); this.engine?.destroy();
this.dialog?.destroy(); this.dialog?.destroy();
@@ -270,7 +271,6 @@ export class BimEngine {
this.sectionPlane?.destroy(); this.sectionPlane?.destroy();
this.sectionAxis?.destroy(); this.sectionAxis?.destroy();
this.sectionBox?.destroy(); this.sectionBox?.destroy();
this.walkControl?.destroy();
this.constructTreeBtn?.destroy(); this.constructTreeBtn?.destroy();
this.aiChat?.destroy(); this.aiChat?.destroy();
this.setting?.destroy(); this.setting?.destroy();

View File

@@ -15,9 +15,8 @@ export const createMapButton = (registry: ManagerRegistry): ButtonConfig => {
icon: getIcon('地图'), icon: getIcon('地图'),
onClick: () => { onClick: () => {
registry.engine3d?.getEngineComponent()?.toggleMiniMap(); registry.engine3d?.getEngineComponent()?.toggleMiniMap();
// 同步漫游面板的小地图按钮状态
const mapState = registry.engine3d?.getEngineComponent()?.getMiniMapState() ?? false; const mapState = registry.engine3d?.getEngineComponent()?.getMiniMapState() ?? false;
registry.walkControl?.panel?.setPlanViewActive(mapState); registry.walkDock?.setPlanViewActive(mapState);
} }
}; };
}; };

View File

@@ -11,8 +11,7 @@ export const createWalkMenuButton = (registry: ManagerRegistry): ButtonConfig =>
align: 'vertical', align: 'vertical',
icon: getIcon('漫游'), icon: getIcon('漫游'),
onClick: () => { onClick: () => {
console.log('漫游按钮被点击'); registry.bottomDock?.toggle('walk');
registry.walkControl?.show();
} }
}; };
}; };

View File

@@ -0,0 +1,351 @@
import { BimTab } from '../tab';
import { BimTree } from '../tree';
import type { TreeNodeConfig } from '../tree/types';
import { TreeNodeCheckState } from '../tree/types';
import type { BimTreeNode } from '../tree/tree-node';
import type { ManagerRegistry } from '../../core/manager-registry';
import { localeManager } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { ThemeConfig } from '../../themes/types';
import type { ComponentTreeDrawerTabComponent } from './types';
interface EngineTreeNode {
name?: string;
id?: string | null;
ids?: string[] | null;
children?: EngineTreeNode[] | null;
isLeaf?: boolean;
}
interface EngineModelData {
name?: string;
url: string;
children?: EngineTreeNode[] | null;
}
interface TransformedNodeData extends EngineTreeNode {
_modelUrl: string;
}
interface ModelParam {
url: string;
ids: number[];
}
function fnv1a32(input: string, seed: number): number {
let h = seed >>> 0;
for (let i = 0; i < input.length; i++) {
const c = input.charCodeAt(i);
h ^= c & 0xff;
h = Math.imul(h, 0x01000193);
h ^= (c >>> 8) & 0xff;
h = Math.imul(h, 0x01000193);
}
return h >>> 0;
}
async function hashIds(ids: string[]): Promise<string> {
const str = JSON.stringify(ids);
const h1 = fnv1a32(str, 0x811c9dc5);
const h2 = fnv1a32(str, 0x811c9dc5 ^ 0x9e3779b9);
return `${h1.toString(16).padStart(8, '0')}${h2.toString(16).padStart(8, '0')}`;
}
function collectModelParams(node: BimTreeNode): ModelParam[] {
const grouped = new Map<string, Set<number>>();
const collect = (current: BimTreeNode): void => {
const data = current.config.data as TransformedNodeData | undefined;
const modelUrl = data?._modelUrl;
if (modelUrl && data?.ids?.length) {
let idSet = grouped.get(modelUrl);
if (!idSet) {
idSet = new Set<number>();
grouped.set(modelUrl, idSet);
}
for (const rawId of data.ids) {
const id = Number(rawId);
if (Number.isFinite(id)) {
idSet.add(id);
}
}
}
for (const child of current.children || []) {
collect(child);
}
};
collect(node);
const result: ModelParam[] = [];
for (const [url, idSet] of grouped) {
if (idSet.size > 0) {
result.push({ url, ids: Array.from(idSet) });
}
}
return result;
}
let nodeIdCounter = 0;
async function transformTreeData(apiData: EngineModelData[]): Promise<TreeNodeConfig[]> {
if (!apiData || apiData.length === 0) return [];
const transformNode = async (node: EngineTreeNode, modelUrl: string): Promise<TreeNodeConfig> => {
const hasChildren = node.children && node.children.length > 0;
let id: string;
if (node.ids?.length) {
id = await hashIds(node.ids);
} else if (node.id) {
id = node.id;
} else {
id = `node_${++nodeIdCounter}`;
}
return {
id,
label: node.name || '未命名',
expanded: false,
checked: true,
children: hasChildren
? await Promise.all(node.children!.map(child => transformNode(child, modelUrl)))
: undefined,
data: {
...node,
_modelUrl: modelUrl
} as TransformedNodeData
};
};
return Promise.all(apiData.map(async (model) => {
const hasChildren = model.children && model.children.length > 0;
const modelUrl = model.url;
return {
id: modelUrl,
label: model.name || '模型',
expanded: true,
checked: true,
children: hasChildren
? await Promise.all(model.children!.map(child => transformNode(child, modelUrl)))
: undefined,
data: {
_modelUrl: modelUrl
} as TransformedNodeData
};
}));
}
export class ComponentTreeDrawerConstructTreeTab implements ComponentTreeDrawerTabComponent {
public readonly element: HTMLElement;
private readonly registry: ManagerRegistry | null;
private readonly mountElement: HTMLElement;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
private unsubscribeShowAll: (() => void) | null = null;
private activeTab: BimTab | null = null;
private levelTree: BimTree | null = null;
private typeTree: BimTree | null = null;
private majorTree: BimTree | null = null;
private refreshToken = 0;
private isRendering = false;
private pendingRefresh = false;
constructor(registry: ManagerRegistry | null) {
this.registry = registry;
this.element = document.createElement('section');
this.element.className = 'component-tree-drawer-construct-tree';
this.element.dataset.tab = 'construct-tree';
this.mountElement = document.createElement('div');
this.mountElement.className = 'component-tree-drawer-construct-tree-mount';
this.element.appendChild(this.mountElement);
}
public init(): void {
this.setTheme(themeManager.getTheme());
this.unsubscribeLocale = localeManager.subscribe(() => {
this.setLocales();
void this.refresh();
});
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
}
public getElement(): HTMLElement {
return this.element;
}
public onShow(): void {
void this.refresh();
}
public setTheme(theme: ThemeConfig): void {
const style = this.element.style;
style.setProperty('--bim-component-tree-page-title', theme.textPrimary);
style.setProperty('--bim-component-tree-page-description', theme.textSecondary);
style.setProperty('--bim-component-tree-content-bg', theme.bgElevated);
}
public setLocales(): void {
// BimTab and BimTree handle their own locale subscriptions.
}
public destroy(): void {
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
if (this.unsubscribeShowAll) {
this.unsubscribeShowAll();
this.unsubscribeShowAll = null;
}
this.destroyCurrentContent();
this.element.remove();
}
private async refresh(): Promise<void> {
if (!this.registry || !this.registry.engine3d) {
return;
}
if (this.isRendering) {
this.pendingRefresh = true;
return;
}
this.isRendering = true;
const currentToken = ++this.refreshToken;
try {
const engineComponent = this.registry.engine3d.getEngineComponent?.();
const levelTreeData = engineComponent?.getLevelTreeData() ?? [];
const typeTreeData = engineComponent?.getTypeTreeData() ?? [];
const majorTreeData = engineComponent?.getMajorTreeData() ?? [];
const [levelTree, typeTree, majorTree] = await Promise.all([
this.createTree(levelTreeData),
this.createTree(typeTreeData),
this.createTree(majorTreeData)
]);
if (currentToken !== this.refreshToken) {
levelTree.destroy();
typeTree.destroy();
majorTree.destroy();
return;
}
this.destroyCurrentContent();
this.levelTree = levelTree;
this.typeTree = typeTree;
this.majorTree = majorTree;
const componentPanel = document.createElement('div');
componentPanel.className = 'component-tree-drawer-construct-tree-panel';
componentPanel.appendChild(levelTree.element);
const typePanel = document.createElement('div');
typePanel.className = 'component-tree-drawer-construct-tree-panel';
typePanel.appendChild(typeTree.element);
const majorPanel = document.createElement('div');
majorPanel.className = 'component-tree-drawer-construct-tree-panel';
majorPanel.appendChild(majorTree.element);
const tabMount = document.createElement('div');
tabMount.className = 'component-tree-drawer-construct-tree-tab-container';
const tab = new BimTab({
container: tabMount,
tabs: [
{ id: 'component', title: 'tab.component', content: componentPanel },
{ id: 'type', title: 'tab.type', content: typePanel },
{ id: 'major', title: 'tab.major', content: majorPanel }
],
activeId: 'component'
});
tab.init();
this.activeTab = tab;
this.mountElement.appendChild(tabMount);
this.resetAllTrees();
this.unsubscribeShowAll = this.registry.on('menu:show-all', () => {
this.resetAllTrees();
});
} finally {
this.isRendering = false;
if (this.pendingRefresh) {
this.pendingRefresh = false;
void this.refresh();
}
}
}
private async createTree(data: EngineModelData[]): Promise<BimTree> {
const transformedData = await transformTreeData(data);
const tree = new BimTree({
data: transformedData,
checkable: true,
indent: 0,
enableSearch: true,
checkStrictly: true,
defaultExpandAll: true,
onNodeCheck: (node) => {
const modelParam = collectModelParams(node);
if (!modelParam.length) return;
if (node.checkState === TreeNodeCheckState.Checked) {
this.registry?.engine3d?.getEngineComponent?.()?.showModel(modelParam);
} else {
this.registry?.engine3d?.getEngineComponent?.()?.hideModels(modelParam);
}
},
onNodeSelect: (node) => {
const modelParam = collectModelParams(node);
if (!modelParam.length) return;
const engineComponent = this.registry?.engine3d?.getEngineComponent?.();
engineComponent?.unhighlightAllModels();
engineComponent?.highlightModel(modelParam);
engineComponent?.viewScaleToModel(modelParam);
},
onNodeDeselect: () => {
this.registry?.engine3d?.getEngineComponent?.()?.unhighlightAllModels();
}
});
tree.init();
return tree;
}
private resetAllTrees(): void {
const engineComponent = this.registry?.engine3d?.getEngineComponent?.();
engineComponent?.showAllModels();
this.levelTree?.checkAllNodes(true);
this.typeTree?.checkAllNodes(true);
this.majorTree?.checkAllNodes(true);
}
private destroyCurrentContent(): void {
if (this.unsubscribeShowAll) {
this.unsubscribeShowAll();
this.unsubscribeShowAll = null;
}
this.activeTab?.destroy();
this.activeTab = null;
this.levelTree?.destroy();
this.levelTree = null;
this.typeTree?.destroy();
this.typeTree = null;
this.majorTree?.destroy();
this.majorTree = null;
this.mountElement.innerHTML = '';
}
}

View File

@@ -0,0 +1,961 @@
.component-tree-drawer {
--bim-component-tree-bg: #ffffff;
--bim-component-tree-blur: 0px;
--bim-component-tree-border: rgba(148, 163, 184, 0.28);
--bim-component-tree-shadow: 0 14px 36px rgba(15, 23, 42, 0.08);
--bim-component-tree-text: #0f172a;
--bim-component-tree-muted: #475569;
--bim-component-tree-tab-bg: #f8fafc;
--bim-component-tree-tab-hover: #f1f5f9;
--bim-component-tree-tab-active: #eff6ff;
--bim-component-tree-tab-active-border: #3b82f6;
--bim-component-tree-tab-active-text: #1d4ed8;
--bim-component-tree-close-bg: #f8fafc;
--bim-component-tree-close-hover: #eef2f7;
--bim-component-tree-page-title: var(--bim-component-tree-text);
--bim-component-tree-page-description: var(--bim-component-tree-muted);
--bim-component-tree-surface: #ffffff;
--bim-component-tree-surface-border: rgba(148, 163, 184, 0.14);
--bim-component-tree-subtab-nav-bg: #e2e8f0;
--bim-component-tree-subtab-nav-border: rgba(148, 163, 184, 0.26);
--bim-component-tree-subtab-text: #334155;
--bim-component-tree-subtab-hover-bg: rgba(255, 255, 255, 0.72);
--bim-component-tree-subtab-hover-text: #0f172a;
--bim-component-tree-subtab-active-bg: #ffffff;
--bim-component-tree-subtab-active-text: #0f172a;
--bim-component-tree-subtab-active-border: rgba(59, 130, 246, 0.22);
--bim-component-tree-subtab-active-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
--bim-component-tree-tree-bg: #ffffff;
--bim-component-tree-tree-border: rgba(148, 163, 184, 0.18);
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: min(320px, 100%);
display: flex;
flex-direction: column;
border: 1px solid var(--bim-component-tree-border);
border-radius: 0;
background: var(--bim-component-tree-bg);
backdrop-filter: blur(var(--bim-component-tree-blur));
-webkit-backdrop-filter: blur(var(--bim-component-tree-blur));
box-shadow: var(--bim-component-tree-shadow);
color: var(--bim-component-tree-text);
overflow: hidden;
transform: translateX(calc(-100% - 24px));
opacity: 0;
pointer-events: none;
transition: transform 220ms ease, opacity 220ms ease;
z-index: 1200;
}
.component-tree-drawer.is-open {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
.component-tree-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 10px 10px 12px;
border-bottom: 1px solid var(--bim-component-tree-border);
}
.component-tree-drawer-tabs {
min-width: 0;
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.component-tree-drawer-tab {
appearance: none;
border: none;
background: transparent;
color: var(--bim-component-tree-muted);
border-radius: 0;
padding: 8px 14px;
min-height: 34px;
font-size: 14px;
font-weight: 500;
line-height: 1;
cursor: pointer;
transition: color 180ms ease;
position: relative;
}
.component-tree-drawer-tab:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 16px;
background-color: var(--bim-component-tree-border);
}
.component-tree-drawer-tab:hover {
color: var(--bim-component-tree-text);
}
.component-tree-drawer-tab.is-active {
color: var(--bim-component-tree-tab-active-text);
font-weight: 600;
}
.component-tree-drawer-close {
flex: 0 0 auto;
width: 44px;
height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 4px;
background: var(--bim-component-tree-close-bg);
color: var(--bim-component-tree-text);
cursor: pointer;
transition: background-color 180ms ease, border-color 180ms ease, color 180ms ease;
margin-left: 8px;
}
.component-tree-drawer-close:hover {
background: var(--bim-component-tree-close-hover);
border-color: var(--bim-component-tree-border);
box-shadow: inset 0 0 0 1px var(--bim-component-tree-border);
}
.component-tree-drawer-close svg {
width: 40px;
height: 40px;
}
.component-tree-drawer-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
padding: 10px 12px 12px;
overflow: hidden;
}
.component-tree-drawer-panel {
display: none;
flex: 1 1 auto;
min-height: 0;
height: 100%;
overflow: hidden;
}
.component-tree-drawer-panel.is-active {
display: flex;
flex-direction: column;
}
.component-tree-drawer-page {
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 8px;
padding: 12px;
border: 1px solid var(--bim-component-tree-surface-border);
border-radius: 8px;
background: var(--bim-component-tree-surface);
}
.component-tree-drawer-page-title {
margin: 0;
font-size: 15px;
line-height: 1.3;
color: var(--bim-component-tree-page-title);
}
.component-tree-drawer-page-description {
margin: 0;
font-size: 12px;
line-height: 1.6;
color: var(--bim-component-tree-page-description);
}
.component-tree-drawer-pin-page {
gap: 0;
padding: 0;
border: none;
background: transparent;
}
.component-tree-drawer-add-pin-btn {
appearance: none;
border: none;
background: var(--bim-pin-primary, #3b82f6);
color: #ffffff;
border-radius: 10px;
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
line-height: 1;
cursor: pointer;
transition: all 200ms ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
box-shadow: 0 1px 3px rgba(59, 130, 246, 0.3);
}
.component-tree-drawer-add-pin-btn:hover {
background: var(--bim-pin-primary-hover, #2563eb);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.component-tree-drawer-add-pin-btn:active {
transform: translateY(0);
}
.component-tree-drawer-add-pin-btn svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.component-tree-drawer-pin-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 48px 20px;
text-align: center;
}
.component-tree-drawer-pin-empty-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
color: var(--bim-pin-muted, #94a3b8);
opacity: 0.5;
}
.component-tree-drawer-pin-empty-icon svg {
width: 100%;
height: 100%;
}
.component-tree-drawer-pin-empty-title {
font-size: 15px;
font-weight: 600;
color: var(--bim-pin-text, #1e293b);
}
.component-tree-drawer-pin-empty-desc {
font-size: 13px;
color: var(--bim-pin-muted, #64748b);
line-height: 1.5;
}
.component-tree-drawer-pin-list {
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
overflow-x: hidden;
max-height: 100%;
padding: 0 4px;
}
.component-tree-drawer-pin-item {
background: var(--bim-pin-card-bg, #ffffff);
border: 1px solid var(--bim-pin-border, #e2e8f0);
border-radius: 12px;
padding: 2px;
cursor: pointer;
transition: all 200ms ease;
box-shadow: var(--bim-pin-shadow, 0 1px 3px rgba(0, 0, 0, 0.05));
overflow: hidden;
}
.component-tree-drawer-pin-item:hover {
border-color: var(--bim-component-tree-tab-active-border, #3b82f6);
box-shadow: var(--bim-pin-shadow-hover, 0 4px 12px rgba(0, 0, 0, 0.1));
transform: translateY(-1px);
}
.component-tree-drawer-pin-content {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
min-height: 48px;
}
.component-tree-drawer-pin-icon {
flex: 0 0 auto;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bim-component-tree-tab-active, #eff6ff);
color: var(--bim-component-tree-tab-active-text, #1d4ed8);
border-radius: 10px;
}
.component-tree-drawer-pin-icon svg {
width: 20px;
height: 20px;
}
.component-tree-drawer-pin-info {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.component-tree-drawer-pin-name {
font-size: 14px;
font-weight: 600;
color: var(--bim-pin-text, #1e293b);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
}
.component-tree-drawer-pin-index {
font-size: 11px;
font-weight: 500;
color: var(--bim-pin-muted, #94a3b8);
line-height: 1.3;
}
.component-tree-drawer-pin-actions {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 2px;
opacity: 0;
transition: opacity 180ms ease;
}
.component-tree-drawer-pin-item:hover .component-tree-drawer-pin-actions {
opacity: 1;
}
.component-tree-drawer-pin-action-btn {
appearance: none;
border: none;
background: transparent;
color: var(--bim-pin-muted, #94a3b8);
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
cursor: pointer;
transition: all 180ms ease;
padding: 0;
}
.component-tree-drawer-pin-action-btn:hover {
background: var(--bim-component-tree-tab-hover, #f1f5f9);
color: var(--bim-pin-text, #1e293b);
}
.component-tree-drawer-pin-action-btn svg {
width: 16px;
height: 16px;
}
.component-tree-drawer-pin-edit-wrapper {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
}
.component-tree-drawer-pin-input {
flex: 1 1 auto;
min-width: 0;
appearance: none;
border: 2px solid var(--bim-component-tree-tab-active-border, #3b82f6);
background: var(--bim-pin-bg, #ffffff);
color: var(--bim-pin-text, #1e293b);
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
font-weight: 600;
outline: none;
transition: box-shadow 200ms ease;
}
.component-tree-drawer-pin-input:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.component-tree-drawer-pin-edit-actions {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 4px;
}
.component-tree-drawer-pin-edit-btn {
appearance: none;
border: none;
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
cursor: pointer;
transition: all 180ms ease;
padding: 0;
}
.component-tree-drawer-pin-edit-save {
background: #10b981;
color: #ffffff;
}
.component-tree-drawer-pin-edit-save:hover {
background: #059669;
}
.component-tree-drawer-pin-edit-cancel {
background: #f1f5f9;
color: #64748b;
}
.component-tree-drawer-pin-edit-cancel:hover {
background: #e2e8f0;
color: #475569;
}
.component-tree-drawer-pin-edit-btn svg {
width: 16px;
height: 16px;
}
.component-tree-drawer-construct-tree {
flex: 1;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.component-tree-drawer-construct-tree-mount {
flex: 1;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.component-tree-drawer-construct-tree-tab-container,
.component-tree-drawer-construct-tree-tab-container .bim-tab,
.component-tree-drawer-construct-tree-tab-container .bim-tab__content,
.component-tree-drawer-construct-tree-tab-container .bim-tab__panel,
.component-tree-drawer-construct-tree-panel,
.component-tree-drawer-construct-tree-tab-container .construct-tab__panel-content,
.component-tree-drawer-construct-tree-tab-container .bim-tree {
height: 100%;
min-height: 0;
overflow: hidden;
}
.component-tree-drawer-construct-tree-tab-container .bim-tab {
display: flex;
flex-direction: column;
background: transparent;
}
.component-tree-drawer-construct-tree-tab-container .bim-tab__nav {
flex: 0 0 auto;
gap: 6px;
padding: 4px;
margin: 0 0 8px;
background: var(--bim-component-tree-subtab-nav-bg);
border-color: var(--bim-component-tree-subtab-nav-border);
border-radius: 10px;
}
.component-tree-drawer-construct-tree-tab-container .bim-tab__item {
min-height: 32px;
padding: 7px 10px;
font-size: 12px;
font-weight: 600;
color: var(--bim-component-tree-subtab-text);
border: 1px solid transparent;
}
.component-tree-drawer-construct-tree-tab-container .bim-tab__item:hover {
color: var(--bim-component-tree-subtab-hover-text);
background: var(--bim-component-tree-subtab-hover-bg);
}
.component-tree-drawer-construct-tree-tab-container .bim-tab__item.is-active {
color: var(--bim-component-tree-subtab-active-text);
background: var(--bim-component-tree-subtab-active-bg);
border-color: var(--bim-component-tree-subtab-active-border);
box-shadow: var(--bim-component-tree-subtab-active-shadow);
}
.component-tree-drawer-construct-tree-tab-container .bim-tab__content {
flex: 1;
min-height: 0;
overflow: hidden;
}
.component-tree-drawer-construct-tree-panel {
overflow: hidden;
}
.component-tree-drawer-construct-tree-tab-container .construct-tab__panel-content {
overflow: hidden;
}
.component-tree-drawer-construct-tree-tab-container .bim-tree {
border: 1px solid var(--bim-component-tree-tree-border);
border-radius: 10px;
background: var(--bim-component-tree-tree-bg);
}
.component-tree-drawer-construct-tree-tab-container .bim-tree-search {
padding: 8px 8px 6px;
}
.component-tree-drawer-construct-tree-tab-container .bim-tree-search-input {
height: 28px;
font-size: 12px;
}
.component-tree-drawer-construct-tree-tab-container .bim-tree-content {
flex: 1 1 auto;
min-height: 0;
padding: 4px 6px 8px;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
}
@media (max-width: 768px) {
.component-tree-drawer {
right: 0;
}
.component-tree-drawer-header {
align-items: flex-start;
}
.component-tree-drawer-tabs {
gap: 6px;
}
.component-tree-drawer-tab {
padding: 8px 12px;
min-height: 34px;
}
}
/* ===== View Tab ===== */
.component-tree-drawer-view {
flex: 1;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.component-tree-drawer-view-mount {
flex: 1;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.component-tree-drawer-view-mount .bim-tree {
height: 100%;
min-height: 0;
border: 1px solid var(--bim-component-tree-tree-border);
border-radius: 10px;
background: var(--bim-component-tree-tree-bg);
overflow: hidden;
}
.component-tree-drawer-view-mount .bim-tree-search {
padding: 8px 8px 6px;
}
.component-tree-drawer-view-mount .bim-tree-search-input {
height: 28px;
font-size: 12px;
}
.component-tree-drawer-view-mount .bim-tree-content {
flex: 1 1 auto;
min-height: 0;
padding: 4px 6px 8px;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
}
.component-tree-drawer-view-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 0;
padding: 24px;
font-size: 13px;
color: var(--bim-component-tree-muted);
text-align: center;
}
/* ===== Pin Tab Toolbar & Buttons ===== */
.component-tree-drawer-pin-toolbar {
margin-bottom: 10px;
}
.component-tree-drawer-pin-btn-group {
display: flex;
gap: 6px;
width: 100%;
flex-wrap: nowrap;
}
.component-tree-drawer-add-pin-btn,
.component-tree-drawer-add-folder-btn {
appearance: none;
border: 1px solid var(--bim-component-tree-tree-border);
color: var(--bim-pin-text, #1e293b);
border-radius: 8px;
padding: 0 10px;
height: 30px;
font-size: 12px;
font-weight: 600;
line-height: 1;
cursor: pointer;
transition: background-color 180ms ease, border-color 180ms ease, color 180ms ease, transform 180ms ease;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
background: var(--bim-pin-card-bg, #ffffff);
box-shadow: none;
white-space: nowrap;
flex: 0 0 auto;
min-width: 0;
}
.component-tree-drawer-add-pin-btn svg,
.component-tree-drawer-add-folder-btn svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.component-tree-drawer-add-pin-btn {
background: var(--bim-pin-soft, rgba(59, 130, 246, 0.12));
border-color: color-mix(in srgb, var(--bim-pin-primary, #3b82f6) 26%, var(--bim-component-tree-tree-border));
color: var(--bim-component-tree-tab-active-text, #1d4ed8);
flex: 1 1 auto;
}
.component-tree-drawer-add-pin-btn:hover {
background: color-mix(in srgb, var(--bim-pin-primary, #3b82f6) 14%, var(--bim-pin-card-bg, #ffffff));
border-color: color-mix(in srgb, var(--bim-pin-primary, #3b82f6) 38%, var(--bim-component-tree-tree-border));
}
.component-tree-drawer-add-folder-btn {
background: transparent;
padding-inline: 9px;
}
.component-tree-drawer-add-folder-btn:hover {
background: var(--bim-component-tree-tab-hover, #f1f5f9);
border-color: var(--bim-component-tree-tree-border);
color: var(--bim-pin-text, #1e293b);
}
.component-tree-drawer-add-pin-btn:focus-visible,
.component-tree-drawer-add-folder-btn:focus-visible,
.pin-node-action-btn:focus-visible,
.pin-node-edit-btn:focus-visible,
.pin-node-edit-input:focus-visible {
outline: 2px solid color-mix(in srgb, var(--bim-pin-primary, #3b82f6) 60%, white);
outline-offset: 2px;
}
.component-tree-drawer-add-pin-btn:active,
.component-tree-drawer-add-folder-btn:active {
transform: translateY(-1px);
}
/* ===== Pin Tree Container ===== */
.component-tree-drawer-pin-tree-container {
flex: 1;
min-height: 0;
overflow: hidden;
}
.component-tree-drawer-pin-tree-container .bim-tree {
height: 100%;
border: 1px solid var(--bim-component-tree-tree-border);
border-radius: 10px;
background: var(--bim-component-tree-tree-bg);
overflow: hidden;
}
.component-tree-drawer-pin-tree-container .bim-tree-content {
flex: 1 1 auto;
min-height: 0;
padding: 6px 8px 10px;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
}
.component-tree-drawer-pin-tree-container .bim-tree-node-content {
min-height: 34px;
border-radius: 8px;
padding-right: 6px;
}
.component-tree-drawer-pin-tree-container .bim-tree-node-content.is-selected {
background: color-mix(in srgb, var(--bim-pin-primary, #3b82f6) 12%, var(--bim-component-tree-tree-bg));
}
.component-tree-drawer-pin-tree-container .bim-tree-title {
font-size: 13px;
font-weight: 500;
}
.component-tree-drawer-pin-tree-container .bim-tree-node-actions {
display: flex;
}
/* ===== Pin Node Actions ===== */
.pin-node-actions {
display: flex;
align-items: center;
gap: 4px;
opacity: 0;
transition: opacity 180ms ease;
}
.bim-tree-node-content:hover .pin-node-actions,
.bim-tree-node-content.is-selected .pin-node-actions,
.bim-tree-node-content:focus-within .pin-node-actions {
opacity: 1;
}
.pin-node-action-btn {
appearance: none;
border: none;
background: transparent;
color: var(--bim-pin-muted, #94a3b8);
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
transition: all 180ms ease;
padding: 0;
}
.pin-node-action-btn:hover {
background: var(--bim-component-tree-tab-hover, #f1f5f9);
color: var(--bim-pin-text, #1e293b);
}
.pin-node-action-btn svg {
width: 13px;
height: 13px;
}
/* ===== Pin Node Edit Mode ===== */
.pin-node-edit-wrapper {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
}
.pin-node-edit-input {
flex: 1;
min-width: 0;
appearance: none;
border: 1px solid color-mix(in srgb, var(--bim-pin-primary, #3b82f6) 35%, var(--bim-component-tree-tree-border));
background: var(--bim-pin-bg, #ffffff);
color: var(--bim-pin-text, #1e293b);
border-radius: 6px;
padding: 5px 8px;
font-size: 13px;
font-weight: 500;
outline: none;
transition: box-shadow 200ms ease;
}
.pin-node-edit-input:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.pin-node-edit-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.pin-node-edit-btn {
appearance: none;
border: none;
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
transition: all 180ms ease;
padding: 0;
}
.pin-node-edit-btn svg {
width: 12px;
height: 12px;
}
.pin-node-edit-save {
background: #10b981;
color: #ffffff;
}
.pin-node-edit-save:hover {
background: #059669;
}
.pin-node-edit-cancel {
background: #f1f5f9;
color: #64748b;
}
.pin-node-edit-cancel:hover {
background: #e2e8f0;
color: #475569;
}
.pin-delete-dialog {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
min-width: 0;
padding: 20px 22px 18px;
}
.pin-delete-dialog-symbol {
width: 52px;
height: 52px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: color-mix(in srgb, #f59e0b 12%, var(--bim-pin-card-bg, #ffffff));
color: #d97706;
font-size: 30px;
font-weight: 700;
line-height: 1;
}
.pin-delete-dialog-text {
font-size: 14px;
line-height: 1.7;
color: var(--bim-pin-text, #1e293b);
word-break: break-word;
text-align: center;
}
.pin-delete-dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
width: 100%;
margin-top: 2px;
}
.pin-delete-dialog-btn {
min-width: 88px;
height: 36px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid var(--bim-component-tree-tree-border);
background: var(--bim-pin-card-bg, #ffffff);
color: var(--bim-pin-text, #1e293b);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background-color 180ms ease, border-color 180ms ease, color 180ms ease;
}
.pin-delete-dialog-btn.is-secondary:hover {
background: var(--bim-component-tree-tab-hover, #f1f5f9);
}
.pin-delete-dialog-btn.is-danger {
border-color: color-mix(in srgb, #ef4444 30%, var(--bim-component-tree-tree-border));
background: color-mix(in srgb, #ef4444 10%, var(--bim-pin-card-bg, #ffffff));
color: #b91c1c;
}
.pin-delete-dialog-btn.is-danger:hover {
background: color-mix(in srgb, #ef4444 16%, var(--bim-pin-card-bg, #ffffff));
}
.pin-delete-dialog-btn:focus-visible {
outline: 2px solid color-mix(in srgb, var(--bim-pin-primary, #3b82f6) 60%, white);
outline-offset: 2px;
}
@media (max-width: 560px) {
.component-tree-drawer-pin-btn-group {
width: 100%;
}
.component-tree-drawer-add-pin-btn,
.component-tree-drawer-add-folder-btn {
flex: 1 1 0;
}
}
@media (prefers-reduced-motion: reduce) {
.component-tree-drawer,
.component-tree-drawer-tab,
.component-tree-drawer-close {
transition: none;
}
}

View File

@@ -0,0 +1,245 @@
import './index.css';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { ThemeConfig } from '../../themes/types';
import type { IBimComponent } from '../../types/component';
import { getIcon } from '../../utils/icon-manager';
import type { ManagerRegistry } from '../../core/manager-registry';
import { ComponentTreeDrawerConstructTreeTab } from './construct-tree-tab';
import { ComponentTreeDrawerPinTab } from './pin-tab';
import type {
ComponentTreeDrawerOptions,
ComponentTreeDrawerTabApi,
ComponentTreeDrawerTabComponent,
ComponentTreeDrawerTabId
} from './types';
import { ComponentTreeDrawerViewTab } from './view-tab';
const TAB_ORDER: ComponentTreeDrawerTabId[] = ['view', 'pin', 'construct-tree'];
export class ComponentTreeDrawer implements IBimComponent {
public readonly element: HTMLElement;
private readonly options: ComponentTreeDrawerOptions;
private readonly headerElement: HTMLElement;
private readonly tabsElement: HTMLElement;
private readonly closeButton: HTMLButtonElement;
private readonly bodyElement: HTMLElement;
private readonly tabButtons: Map<ComponentTreeDrawerTabId, HTMLButtonElement> = new Map();
private readonly panelElements: Map<ComponentTreeDrawerTabId, HTMLElement> = new Map();
private readonly tabInstances: Record<ComponentTreeDrawerTabId, ComponentTreeDrawerTabComponent>;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
private activeTab: ComponentTreeDrawerTabId;
private open = false;
constructor(options: ComponentTreeDrawerOptions = {}) {
this.options = options;
this.activeTab = options.defaultTab ?? 'view';
const registry = options.registry as ManagerRegistry | undefined;
this.tabInstances = {
view: new ComponentTreeDrawerViewTab(registry ?? null),
pin: new ComponentTreeDrawerPinTab(registry ?? null),
'construct-tree': new ComponentTreeDrawerConstructTreeTab(registry ?? null)
};
this.element = document.createElement('aside');
this.element.className = 'component-tree-drawer';
this.element.setAttribute('aria-hidden', 'true');
this.headerElement = document.createElement('div');
this.headerElement.className = 'component-tree-drawer-header';
this.tabsElement = document.createElement('div');
this.tabsElement.className = 'component-tree-drawer-tabs';
this.closeButton = document.createElement('button');
this.closeButton.type = 'button';
this.closeButton.className = 'component-tree-drawer-close';
this.closeButton.innerHTML = getIcon('close');
this.closeButton.addEventListener('click', () => this.close());
this.bodyElement = document.createElement('div');
this.bodyElement.className = 'component-tree-drawer-body';
this.headerElement.appendChild(this.tabsElement);
this.headerElement.appendChild(this.closeButton);
this.element.appendChild(this.headerElement);
this.element.appendChild(this.bodyElement);
this.renderTabs();
this.renderPanels();
}
public init(): void {
const container = this.options.container ?? document.body;
if (!this.element.parentElement) {
container.appendChild(this.element);
}
Object.values(this.tabInstances).forEach((tab) => tab.init());
this.setLocales();
this.setTheme(themeManager.getTheme());
this.applyActiveTab();
this.applyOpenState();
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
}
public setTheme(theme: ThemeConfig): void {
const style = this.element.style;
style.setProperty('--bim-component-tree-bg', theme.bgGlass || theme.bgElevated);
style.setProperty('--bim-component-tree-blur', theme.bgGlassBlur || '18px');
style.setProperty('--bim-component-tree-border', theme.borderDefault);
style.setProperty('--bim-component-tree-shadow', theme.shadowLg);
style.setProperty('--bim-component-tree-text', theme.textPrimary);
style.setProperty('--bim-component-tree-muted', theme.textSecondary);
style.setProperty('--bim-component-tree-tab-bg', theme.componentBg);
style.setProperty('--bim-component-tree-tab-hover', theme.componentBgHover);
style.setProperty('--bim-component-tree-tab-active', theme.primarySubtle);
style.setProperty('--bim-component-tree-tab-active-border', theme.primary);
style.setProperty('--bim-component-tree-tab-active-text', theme.primary);
style.setProperty('--bim-component-tree-close-bg', theme.componentBg);
style.setProperty('--bim-component-tree-close-hover', theme.componentBgHover);
style.setProperty('--bim-component-tree-surface', theme.bgElevated);
style.setProperty('--bim-component-tree-surface-border', theme.borderSubtle);
style.setProperty('--bim-component-tree-subtab-nav-bg', theme.tabBg);
style.setProperty('--bim-component-tree-subtab-nav-border', theme.borderDefault);
style.setProperty('--bim-component-tree-subtab-text', theme.textSecondary);
style.setProperty('--bim-component-tree-subtab-hover-bg', theme.tabItemBgHover);
style.setProperty('--bim-component-tree-subtab-hover-text', theme.textPrimary);
style.setProperty('--bim-component-tree-subtab-active-bg', theme.tabItemBgActive);
style.setProperty('--bim-component-tree-subtab-active-text', theme.tabItemTextActive);
style.setProperty('--bim-component-tree-subtab-active-border', theme.primarySubtle);
style.setProperty('--bim-component-tree-subtab-active-shadow', theme.shadowSm);
style.setProperty('--bim-component-tree-tree-bg', theme.bgElevated);
style.setProperty('--bim-component-tree-tree-border', theme.borderDefault);
}
public setLocales(): void {
for (const tabId of TAB_ORDER) {
const button = this.tabButtons.get(tabId);
if (!button) continue;
button.textContent = this.getTabTitle(tabId);
}
const closeLabel = t('componentTreeDrawer.actions.close');
this.closeButton.setAttribute('aria-label', closeLabel);
this.closeButton.setAttribute('title', closeLabel);
}
public destroy(): void {
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
Object.values(this.tabInstances).forEach((tab) => tab.destroy());
this.tabButtons.clear();
this.panelElements.clear();
this.element.remove();
}
public openDrawer(tabId?: ComponentTreeDrawerTabId): void {
if (tabId) {
this.activeTab = tabId;
this.applyActiveTab();
}
this.open = true;
this.applyOpenState();
void this.tabInstances[this.activeTab].onShow?.();
}
public close(): void {
if (!this.open) return;
this.open = false;
this.applyOpenState();
this.options.onClose?.();
}
public toggle(tabId?: ComponentTreeDrawerTabId): void {
if (this.open) {
this.close();
return;
}
this.openDrawer(tabId);
}
public isOpen(): boolean {
return this.open;
}
public getActiveTab(): ComponentTreeDrawerTabId {
return this.activeTab;
}
public switchTab(tabId: ComponentTreeDrawerTabId): void {
if (this.activeTab === tabId) return;
this.activeTab = tabId;
this.applyActiveTab();
void this.tabInstances[tabId].onShow?.();
this.options.onTabChange?.(tabId);
}
public getViewTabApi(): ComponentTreeDrawerTabApi {
return this.tabInstances.view;
}
public getPinTabApi(): ComponentTreeDrawerTabApi {
return this.tabInstances.pin;
}
public getConstructTreeTabApi(): ComponentTreeDrawerTabApi {
return this.tabInstances['construct-tree'];
}
private renderTabs(): void {
this.tabsElement.innerHTML = '';
for (const tabId of TAB_ORDER) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'component-tree-drawer-tab';
button.dataset.tab = tabId;
button.addEventListener('click', () => this.switchTab(tabId));
this.tabButtons.set(tabId, button);
this.tabsElement.appendChild(button);
}
}
private renderPanels(): void {
this.bodyElement.innerHTML = '';
for (const tabId of TAB_ORDER) {
const panel = document.createElement('div');
panel.className = 'component-tree-drawer-panel';
panel.dataset.tab = tabId;
panel.appendChild(this.tabInstances[tabId].getElement());
this.panelElements.set(tabId, panel);
this.bodyElement.appendChild(panel);
}
}
private applyActiveTab(): void {
for (const tabId of TAB_ORDER) {
const isActive = tabId === this.activeTab;
this.tabButtons.get(tabId)?.classList.toggle('is-active', isActive);
this.panelElements.get(tabId)?.classList.toggle('is-active', isActive);
}
}
private applyOpenState(): void {
this.element.classList.toggle('is-open', this.open);
this.element.setAttribute('aria-hidden', `${!this.open}`);
}
private getTabTitle(tabId: ComponentTreeDrawerTabId): string {
const keyMap: Record<ComponentTreeDrawerTabId, string> = {
view: 'componentTreeDrawer.tabs.view',
pin: 'componentTreeDrawer.tabs.pin',
'construct-tree': 'componentTreeDrawer.tabs.constructTree'
};
return t(keyMap[tabId]);
}
}

View File

@@ -0,0 +1,532 @@
import type { ThemeConfig } from '../../themes/types';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { ManagerRegistry } from '../../core/manager-registry';
import type { DrawingPinRecord } from '../../types/events';
import { getIcon } from '../../utils/icon-manager';
import { BimTree } from '../tree';
import type { BimDialog } from '../dialog';
import type { TreeNodeConfig } from '../tree/types';
import type { ComponentTreeDrawerTabComponent } from './types';
interface PinTreeNode extends DrawingPinRecord {
children?: PinTreeNode[];
}
export class ComponentTreeDrawerPinTab implements ComponentTreeDrawerTabComponent {
public readonly element: HTMLElement;
private readonly registry: ManagerRegistry | null;
private readonly toolbarElement: HTMLElement;
private readonly addPinBtn: HTMLButtonElement;
private readonly addFolderBtn: HTMLButtonElement;
private readonly emptyElement: HTMLElement;
private readonly listContainer: HTMLElement;
private tree: BimTree | null = null;
private records: DrawingPinRecord[] = [];
private nodes: PinTreeNode[] = [];
private editingId: string | null = null;
private deleteDialog: BimDialog | null = null;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
private unsubscribeListUpdated: (() => void) | null = null;
constructor(registry: ManagerRegistry | null) {
this.registry = registry;
this.element = document.createElement('section');
this.element.className = 'component-tree-drawer-page component-tree-drawer-pin-page';
this.element.dataset.tab = 'pin';
this.toolbarElement = document.createElement('div');
this.toolbarElement.className = 'component-tree-drawer-pin-toolbar';
const btnGroup = document.createElement('div');
btnGroup.className = 'component-tree-drawer-pin-btn-group';
this.addPinBtn = document.createElement('button');
this.addPinBtn.type = 'button';
this.addPinBtn.className = 'component-tree-drawer-add-pin-btn';
this.addPinBtn.addEventListener('click', () => this.handleAddPin());
this.addFolderBtn = document.createElement('button');
this.addFolderBtn.type = 'button';
this.addFolderBtn.className = 'component-tree-drawer-add-folder-btn';
this.addFolderBtn.addEventListener('click', () => this.handleAddFolder());
btnGroup.appendChild(this.addPinBtn);
btnGroup.appendChild(this.addFolderBtn);
this.toolbarElement.appendChild(btnGroup);
this.emptyElement = document.createElement('div');
this.emptyElement.className = 'component-tree-drawer-pin-empty';
const emptyIcon = document.createElement('div');
emptyIcon.className = 'component-tree-drawer-pin-empty-icon';
emptyIcon.innerHTML = getIcon('pin');
const emptyTitle = document.createElement('div');
emptyTitle.className = 'component-tree-drawer-pin-empty-title';
const emptyDesc = document.createElement('div');
emptyDesc.className = 'component-tree-drawer-pin-empty-desc';
this.emptyElement.appendChild(emptyIcon);
this.emptyElement.appendChild(emptyTitle);
this.emptyElement.appendChild(emptyDesc);
this.listContainer = document.createElement('div');
this.listContainer.className = 'component-tree-drawer-pin-tree-container';
this.element.appendChild(this.toolbarElement);
this.element.appendChild(this.emptyElement);
this.element.appendChild(this.listContainer);
}
public init(): void {
this.setLocales();
this.setTheme(themeManager.getTheme());
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.unsubscribeListUpdated = this.registry?.on('drawingPin:list-updated', (payload) => {
this.records = this.normalizeRecords(payload.records as DrawingPinRecord[]);
this.nodes = this.buildTree(this.records);
this.renderTree();
}) ?? null;
}
public getElement(): HTMLElement {
return this.element;
}
public setTheme(theme: ThemeConfig): void {
const style = this.element.style;
style.setProperty('--bim-component-tree-page-title', theme.textPrimary);
style.setProperty('--bim-component-tree-page-description', theme.textSecondary);
style.setProperty('--bim-pin-primary', theme.primary);
style.setProperty('--bim-pin-primary-hover', theme.primaryHover);
style.setProperty('--bim-pin-bg', theme.bgElevated);
style.setProperty('--bim-pin-card-bg', theme.componentBg);
style.setProperty('--bim-pin-card-hover', theme.componentBgHover);
style.setProperty('--bim-pin-border', theme.borderSubtle);
style.setProperty('--bim-pin-text', theme.textPrimary);
style.setProperty('--bim-pin-muted', theme.textSecondary);
style.setProperty('--bim-pin-shadow', theme.shadowSm);
style.setProperty('--bim-pin-shadow-hover', theme.shadowMd);
style.setProperty('--bim-pin-soft', theme.primarySubtle);
}
public setLocales(): void {
this.addPinBtn.innerHTML = `${getIcon('plus')} <span>${t('componentTreeDrawer.actions.addPin')}</span>`;
this.addFolderBtn.innerHTML = `${getIcon('folder')} <span>${t('componentTreeDrawer.actions.addFolder')}</span>`;
const emptyTitle = this.emptyElement.querySelector('.component-tree-drawer-pin-empty-title');
const emptyDesc = this.emptyElement.querySelector('.component-tree-drawer-pin-empty-desc');
if (emptyTitle) emptyTitle.textContent = t('componentTreeDrawer.empty.pinTitle');
if (emptyDesc) emptyDesc.textContent = t('componentTreeDrawer.empty.pinDesc');
}
public destroy(): void {
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
if (this.unsubscribeListUpdated) {
this.unsubscribeListUpdated();
this.unsubscribeListUpdated = null;
}
this.closeDeleteDialog();
this.tree?.destroy();
this.tree = null;
this.element.remove();
}
private normalizeRecords(records: DrawingPinRecord[]): DrawingPinRecord[] {
return [...records].map((record) => ({
...record,
parentId: record.parentId ?? null,
}));
}
private buildTree(records: DrawingPinRecord[]): PinTreeNode[] {
const nodeMap = new Map<string, PinTreeNode>();
const roots: PinTreeNode[] = [];
records.forEach((record) => {
nodeMap.set(record.id, { ...record, children: [] });
});
records.forEach((record) => {
const node = nodeMap.get(record.id);
if (!node) return;
const parentId = record.parentId ?? null;
if (parentId && nodeMap.has(parentId)) {
nodeMap.get(parentId)?.children?.push(node);
return;
}
roots.push(node);
});
const sortDeep = (nodes: PinTreeNode[]) => {
nodes.sort((a, b) => a.seq - b.seq || a.name.localeCompare(b.name));
nodes.forEach((node) => {
if (node.children?.length) {
sortDeep(node.children);
} else {
delete node.children;
}
});
};
sortDeep(roots);
return roots;
}
private handleAddPin(parentId: string | null = null): void {
const engineComponent = this.registry?.engine3d?.getEngineComponent?.();
if (!engineComponent) {
console.warn('[PinTab] Engine component not available.');
return;
}
engineComponent.createDrawingPin((data: any) => {
const siblingCount = this.getSiblingCount(parentId, 'pin');
const record: DrawingPinRecord = {
id: `pin_${Date.now()}`,
parentId,
name: `图钉${siblingCount + 1}`,
type: 'pin',
seq: this.getNextSeq(parentId),
data,
};
this.registry?.emit('drawingPin:create', { record });
});
}
private handleAddFolder(parentId: string | null = null): void {
const siblingCount = this.getSiblingCount(parentId, 'folder');
const record: DrawingPinRecord = {
id: `folder_${Date.now()}`,
parentId,
name: `文件夹${siblingCount + 1}`,
type: 'folder',
seq: this.getNextSeq(parentId),
};
this.registry?.emit('drawingPin:create', { record });
}
private getNextSeq(parentId: string | null): number {
return this.records.filter((record) => (record.parentId ?? null) === parentId).length;
}
private getSiblingCount(parentId: string | null, type: DrawingPinRecord['type']): number {
return this.records.filter((record) => record.type === type && (record.parentId ?? null) === parentId).length;
}
private findNodeById(id: string, nodes: PinTreeNode[] = this.nodes): PinTreeNode | null {
for (const node of nodes) {
if (node.id === id) return node;
if (node.children?.length) {
const found = this.findNodeById(id, node.children);
if (found) return found;
}
}
return null;
}
private renderTree(): void {
this.tree?.destroy();
this.tree = null;
if (this.nodes.length === 0) {
this.emptyElement.style.display = 'flex';
this.listContainer.style.display = 'none';
return;
}
this.emptyElement.style.display = 'none';
this.listContainer.style.display = 'block';
this.tree = new BimTree({
data: this.toTreeConfig(this.nodes),
checkable: false,
checkStrictly: false,
defaultExpandAll: true,
enableSearch: true,
searchPlaceholder: 'tree.searchPlaceholder',
renderActions: (config) => this.renderNodeActions(config.data as PinTreeNode),
onNodeSelect: (node) => {
const data = node.config.data as PinTreeNode;
if (data.type !== 'pin' || !data.data) return;
const engineComponent = this.registry?.engine3d?.getEngineComponent?.();
if (engineComponent) {
void engineComponent.restoreDrawingPin(data.data);
}
},
});
this.tree.init();
this.listContainer.innerHTML = '';
this.listContainer.appendChild(this.tree.element);
}
private toTreeConfig(nodes: PinTreeNode[]): TreeNodeConfig[] {
return nodes.map((node) => ({
id: node.id,
label: node.name,
icon: node.type === 'folder' ? getIcon('folder') : getIcon('pin'),
expanded: true,
data: node,
children: node.children?.length ? this.toTreeConfig(node.children) : undefined,
clickAction: node.type === 'folder' ? 'expand' : 'select',
}));
}
private renderNodeActions(node: PinTreeNode): HTMLElement {
const actions = document.createElement('div');
actions.className = 'pin-node-actions';
if (node.type === 'folder') {
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.className = 'pin-node-action-btn';
editBtn.title = t('componentTreeDrawer.actions.edit');
editBtn.setAttribute('aria-label', t('componentTreeDrawer.actions.edit'));
editBtn.innerHTML = getIcon('edit');
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.startEdit(node.id);
});
actions.appendChild(editBtn);
const addPinBtn = document.createElement('button');
addPinBtn.type = 'button';
addPinBtn.className = 'pin-node-action-btn';
addPinBtn.title = t('componentTreeDrawer.actions.addPin');
addPinBtn.setAttribute('aria-label', t('componentTreeDrawer.actions.addPin'));
addPinBtn.innerHTML = getIcon('plus');
addPinBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.handleAddPin(node.id);
});
actions.appendChild(addPinBtn);
const deleteFolderBtn = document.createElement('button');
deleteFolderBtn.type = 'button';
deleteFolderBtn.className = 'pin-node-action-btn';
deleteFolderBtn.title = t('componentTreeDrawer.actions.delete');
deleteFolderBtn.setAttribute('aria-label', t('componentTreeDrawer.actions.delete'));
deleteFolderBtn.innerHTML = getIcon('delete');
deleteFolderBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.openDeleteFolderDialog(node);
});
actions.appendChild(deleteFolderBtn);
return actions;
}
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.className = 'pin-node-action-btn';
editBtn.title = t('componentTreeDrawer.actions.edit');
editBtn.setAttribute('aria-label', t('componentTreeDrawer.actions.edit'));
editBtn.innerHTML = getIcon('edit');
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.startEdit(node.id);
});
actions.appendChild(editBtn);
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'pin-node-action-btn';
deleteBtn.title = t('componentTreeDrawer.actions.delete');
deleteBtn.setAttribute('aria-label', t('componentTreeDrawer.actions.delete'));
deleteBtn.innerHTML = getIcon('delete');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.registry?.emit('drawingPin:delete', { id: node.id });
});
actions.appendChild(deleteBtn);
return actions;
}
private startEdit(id: string): void {
if (this.editingId === id) return;
this.editingId = id;
const node = this.findNodeById(id);
if (!node) return;
const treeNode = this.tree?.getNode(id);
if (!treeNode) return;
const contentEl = treeNode.element.querySelector('.bim-tree-node-content') as HTMLElement | null;
if (!contentEl) return;
const titleEl = contentEl.querySelector('.bim-tree-title') as HTMLElement | null;
const actionsEl = contentEl.querySelector('.bim-tree-node-actions') as HTMLElement | null;
if (!titleEl || !actionsEl) return;
titleEl.style.display = 'none';
actionsEl.style.display = 'none';
const editWrapper = document.createElement('div');
editWrapper.className = 'pin-node-edit-wrapper';
const input = document.createElement('input');
input.type = 'text';
input.className = 'pin-node-edit-input';
input.value = node.name;
input.maxLength = 12;
const editActions = document.createElement('div');
editActions.className = 'pin-node-edit-actions';
const saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.className = 'pin-node-edit-btn pin-node-edit-save';
saveBtn.innerHTML = getIcon('check');
saveBtn.title = t('componentTreeDrawer.actions.save');
saveBtn.setAttribute('aria-label', t('componentTreeDrawer.actions.save'));
saveBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.saveEdit(id, input.value);
});
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'pin-node-edit-btn pin-node-edit-cancel';
cancelBtn.innerHTML = getIcon('close');
cancelBtn.title = t('componentTreeDrawer.actions.cancel');
cancelBtn.setAttribute('aria-label', t('componentTreeDrawer.actions.cancel'));
cancelBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.cancelEdit();
});
editActions.appendChild(saveBtn);
editActions.appendChild(cancelBtn);
editWrapper.appendChild(input);
editWrapper.appendChild(editActions);
contentEl.appendChild(editWrapper);
requestAnimationFrame(() => {
input.focus();
input.select();
});
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
this.saveEdit(id, input.value);
} else if (e.key === 'Escape') {
e.preventDefault();
this.cancelEdit();
}
};
input.addEventListener('keydown', handleKeydown);
(contentEl as HTMLElement & { _editWrapper?: HTMLElement; _keydownHandler?: (e: KeyboardEvent) => void })._editWrapper = editWrapper;
(contentEl as HTMLElement & { _editWrapper?: HTMLElement; _keydownHandler?: (e: KeyboardEvent) => void })._keydownHandler = handleKeydown;
}
private saveEdit(id: string, newName: string): void {
const node = this.findNodeById(id);
if (!node) return;
const trimmedName = newName.trim() || node.name;
const finalName = trimmedName.slice(0, 12);
if (finalName === node.name) {
this.cancelEdit();
return;
}
this.registry?.emit('drawingPin:update', {
id,
patch: {
name: finalName,
},
});
this.editingId = null;
}
private cancelEdit(): void {
this.editingId = null;
this.renderTree();
}
private countPins(nodes: PinTreeNode[]): number {
let total = 0;
nodes.forEach((node) => {
if (node.type === 'pin') total += 1;
if (node.children?.length) {
total += this.countPins(node.children);
}
});
return total;
}
private openDeleteFolderDialog(node: PinTreeNode): void {
this.closeDeleteDialog();
const content = document.createElement('div');
content.className = 'pin-delete-dialog';
const icon = document.createElement('div');
icon.className = 'pin-delete-dialog-symbol';
icon.textContent = '!';
const message = document.createElement('div');
message.className = 'pin-delete-dialog-text';
message.textContent = `${t('componentTreeDrawer.dialogs.deleteFolderMessagePrefix')}${node.name}${t('componentTreeDrawer.dialogs.deleteFolderMessageSuffix')}`;
const actions = document.createElement('div');
actions.className = 'pin-delete-dialog-actions';
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'pin-delete-dialog-btn is-secondary';
cancelBtn.textContent = t('componentTreeDrawer.actions.cancel');
const confirmBtn = document.createElement('button');
confirmBtn.type = 'button';
confirmBtn.className = 'pin-delete-dialog-btn is-danger';
confirmBtn.textContent = t('componentTreeDrawer.dialogs.confirm');
cancelBtn.addEventListener('click', () => this.closeDeleteDialog());
confirmBtn.addEventListener('click', () => {
this.registry?.emit('drawingPin:delete', { id: node.id });
this.closeDeleteDialog();
});
actions.appendChild(cancelBtn);
actions.appendChild(confirmBtn);
content.appendChild(icon);
content.appendChild(message);
content.appendChild(actions);
this.deleteDialog = this.registry?.dialog?.create({
title: 'componentTreeDrawer.dialogs.noticeTitle',
content,
width: 360,
height: 'auto',
position: 'center',
draggable: false,
resizable: false,
}) ?? null;
}
private closeDeleteDialog(): void {
if (!this.deleteDialog) return;
this.deleteDialog.close();
this.deleteDialog = null;
}
}

View File

@@ -0,0 +1,18 @@
import type { IBimComponent } from '../../types/component';
export type ComponentTreeDrawerTabId = 'view' | 'pin' | 'construct-tree';
export interface ComponentTreeDrawerTabApi {
getElement(): HTMLElement;
onShow?(): void | Promise<void>;
}
export interface ComponentTreeDrawerTabComponent extends IBimComponent, ComponentTreeDrawerTabApi {}
export interface ComponentTreeDrawerOptions {
container?: HTMLElement;
defaultTab?: ComponentTreeDrawerTabId;
onClose?: () => void;
onTabChange?: (tabId: ComponentTreeDrawerTabId) => void;
registry?: unknown;
}

View File

@@ -0,0 +1,181 @@
import type { ThemeConfig } from '../../themes/types';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { ManagerRegistry } from '../../core/manager-registry';
import { BimTree } from '../tree';
import type { TreeNodeConfig } from '../tree/types';
import type { ComponentTreeDrawerTabComponent } from './types';
import type { ModelViewTree, ViewItem } from '../engine/types';
type ViewCategory = 'ceilingPlans' | 'elevations' | 'floorPlans' | 'sections';
const CATEGORY_LABEL_MAP: Record<ViewCategory, string> = {
ceilingPlans: 'viewTab.category.ceilingPlans',
elevations: 'viewTab.category.elevations',
floorPlans: 'viewTab.category.floorPlans',
sections: 'viewTab.category.sections',
};
function transformViewTreeData(models: ModelViewTree[]): TreeNodeConfig[] {
if (!models || models.length === 0) return [];
return models.map((model) => {
const viewCategories: TreeNodeConfig[] = [];
(Object.keys(model.views) as ViewCategory[]).forEach((category) => {
const items = model.views[category];
if (!items || items.length === 0) return;
viewCategories.push({
id: `${model.url}::${category}`,
label: t(CATEGORY_LABEL_MAP[category]),
clickAction: 'expand',
children: items.map((item: ViewItem) => ({
id: `${model.url}::${category}::${item.id}`,
label: item.name,
data: {
url: item.url,
viewId: item.id,
},
})),
});
});
return {
id: model.url,
label: model.name || model.url,
expanded: true,
clickAction: 'expand',
children: viewCategories,
};
});
}
export class ComponentTreeDrawerViewTab implements ComponentTreeDrawerTabComponent {
public readonly element: HTMLElement;
private readonly registry: ManagerRegistry | null;
private readonly mountElement: HTMLElement;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
private tree: BimTree | null = null;
private refreshToken = 0;
private isRendering = false;
private pendingRefresh = false;
constructor(registry: ManagerRegistry | null) {
this.registry = registry;
this.element = document.createElement('section');
this.element.className = 'component-tree-drawer-view';
this.element.dataset.tab = 'view';
this.mountElement = document.createElement('div');
this.mountElement.className = 'component-tree-drawer-view-mount';
this.element.appendChild(this.mountElement);
}
public init(): void {
this.setTheme(themeManager.getTheme());
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
}
public getElement(): HTMLElement {
return this.element;
}
public onShow(): void {
void this.refresh();
}
public setTheme(theme: ThemeConfig): void {
const style = this.element.style;
style.setProperty('--bim-view-tab-text', theme.textPrimary);
style.setProperty('--bim-view-tab-muted', theme.textSecondary);
style.setProperty('--bim-view-tab-bg', theme.bgElevated);
}
public setLocales(): void {
void this.refresh();
}
public destroy(): void {
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
this.destroyCurrentContent();
this.element.remove();
}
private async refresh(): Promise<void> {
if (!this.registry || !this.registry.engine3d) {
return;
}
if (this.isRendering) {
this.pendingRefresh = true;
return;
}
this.isRendering = true;
const currentToken = ++this.refreshToken;
try {
const viewTreeData = this.registry.engine3d.getEngineComponent?.()?.getAllViewTreeData() ?? [];
const transformedData = transformViewTreeData(viewTreeData);
if (currentToken !== this.refreshToken) {
return;
}
this.destroyCurrentContent();
if (transformedData.length === 0) {
this.renderEmptyState();
return;
}
const tree = new BimTree({
data: transformedData,
checkable: false,
indent: 0,
enableSearch: true,
defaultExpandAll: false,
onNodeSelect: (node) => {
const data = node.config.data as { url?: string; viewId?: string } | undefined;
if (data?.url && data?.viewId) {
this.registry?.engine3d?.getEngineComponent?.()?.openView(data.url, data.viewId);
}
},
});
tree.init();
this.tree = tree;
this.mountElement.appendChild(tree.element);
} finally {
this.isRendering = false;
if (this.pendingRefresh) {
this.pendingRefresh = false;
void this.refresh();
}
}
}
private renderEmptyState(): void {
const emptyElement = document.createElement('div');
emptyElement.className = 'component-tree-drawer-view-empty';
emptyElement.textContent = t('viewTab.empty');
this.mountElement.appendChild(emptyElement);
}
private destroyCurrentContent(): void {
this.tree?.destroy();
this.tree = null;
this.mountElement.innerHTML = '';
}
}

View File

@@ -1,13 +1,13 @@
.bim-dialog { .bim-dialog {
position: absolute; position: absolute;
background-color: var(--bim-bg-elevated); background: var(--bim-dialog-bg, var(--bim-bg-elevated));
border: 1px solid var(--bim-border-default); border: 1px solid var(--bim-dialog-border-color, var(--bim-border-default));
border-radius: var(--bim-panel-radius, 12px); border-radius: var(--bim-dialog-radius, 4px);
box-shadow: var(--bim-shadow-lg); box-shadow: var(--bim-shadow-lg);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 10001; z-index: 10001;
color: var(--bim-dialog-title-color, var(--bim-text-primary)); color: var(--bim-dialog-text-color, var(--bim-text-secondary));
overflow: hidden; overflow: hidden;
min-width: 200px; min-width: 200px;
min-height: 100px; min-height: 100px;
@@ -15,16 +15,19 @@
} }
.bim-dialog-header { .bim-dialog-header {
height: 40px; position: relative;
background-color: var(--bim-bg-inset); min-height: 58px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 12px; gap: 12px;
padding: 0 10px 0 18px;
cursor: default; cursor: default;
user-select: none; user-select: none;
border-bottom: 1px solid var(--bim-border-default);
flex-shrink: 0; flex-shrink: 0;
border-bottom: 1px solid var(--bim-border-subtle, var(--bim-dialog-border-color, var(--bim-border-default)));
box-shadow: var(--bim-shadow-sm);
z-index: 1;
} }
.bim-dialog-header.draggable { .bim-dialog-header.draggable {
@@ -32,24 +35,42 @@
} }
.bim-dialog-title { .bim-dialog-title {
font-size: 14px; flex: 1;
font-weight: 500; min-width: 0;
font-size: 18px;
font-weight: 300;
line-height: 1.2;
letter-spacing: 0.01em;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: var(--bim-dialog-title-color); color: var(--bim-dialog-title-color, var(--bim-text-primary));
} }
.bim-dialog-close { .bim-dialog-close {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
cursor: pointer; cursor: pointer;
font-size: 18px; color: var(--bim-text-secondary);
color: var(--bim-text-tertiary); border-radius: 4px;
line-height: 1; transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
flex-shrink: 0;
margin-left: 8px; margin-left: 8px;
} }
.bim-dialog-close:hover { .bim-dialog-close:hover {
color: var(--bim-text-primary); color: var(--bim-text-primary);
background-color: var(--bim-component-bg-hover, rgba(15, 23, 42, 0.06));
box-shadow: inset 0 0 0 1px var(--bim-border-subtle, var(--bim-dialog-border-color, var(--bim-border-default)));
}
.bim-dialog-close svg {
display: block;
width: 20px;
height: 20px;
} }
.bim-dialog-content { .bim-dialog-content {
@@ -57,30 +78,50 @@
overflow: auto; overflow: auto;
font-size: 14px; font-size: 14px;
color: var(--bim-dialog-text-color, var(--bim-text-secondary)); color: var(--bim-dialog-text-color, var(--bim-text-secondary));
background: var(--bim-dialog-bg, var(--bim-bg-elevated));
} }
/* 缩放句柄 */ /* 缩放句柄 */
.bim-dialog-resize-handle { .bim-dialog-resize-handle {
position: absolute; position: absolute;
width: 10px; width: 28px;
height: 10px; height: 28px;
bottom: 0; right: 2px;
right: 0; bottom: 2px;
cursor: se-resize; cursor: se-resize;
z-index: 10; z-index: 10;
border-radius: 4px 0 4px 0;
transition: background-color 0.2s ease;
}
.bim-dialog-resize-handle:hover {
background: var(--bim-component-bg-hover, rgba(15, 23, 42, 0.04));
} }
.bim-dialog-resize-handle::after { .bim-dialog-resize-handle::after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: 3px; right: 7px;
right: 3px; bottom: 7px;
width: 6px; width: 11px;
height: 6px; height: 11px;
border-right: 2px solid var(--bim-text-tertiary); border-right: 2px solid var(--bim-text-tertiary);
border-bottom: 2px solid var(--bim-text-tertiary); border-bottom: 2px solid var(--bim-text-tertiary);
} }
.bim-dialog-resize-handle:hover::after { .bim-dialog-resize-handle::before {
content: '';
position: absolute;
right: 11px;
bottom: 11px;
width: 6px;
height: 6px;
border-right: 2px solid var(--bim-text-tertiary);
border-bottom: 2px solid var(--bim-text-tertiary);
opacity: 0.85;
}
.bim-dialog-resize-handle:hover::after,
.bim-dialog-resize-handle:hover::before {
border-color: var(--bim-text-primary); border-color: var(--bim-text-primary);
} }

View File

@@ -69,6 +69,9 @@ export class BimDialog implements IBimComponent {
style.setProperty('--bim-text-secondary', theme.textSecondary); style.setProperty('--bim-text-secondary', theme.textSecondary);
style.setProperty('--bim-text-tertiary', theme.textTertiary); style.setProperty('--bim-text-tertiary', theme.textTertiary);
style.setProperty('--bim-border-default', theme.borderDefault); style.setProperty('--bim-border-default', theme.borderDefault);
style.setProperty('--bim-border-subtle', theme.borderSubtle);
style.setProperty('--bim-component-bg-hover', theme.componentBgHover);
style.setProperty('--bim-shadow-sm', theme.shadowSm);
style.setProperty('--bim-shadow-lg', theme.shadowLg); style.setProperty('--bim-shadow-lg', theme.shadowLg);
} }
@@ -110,7 +113,7 @@ export class BimDialog implements IBimComponent {
} }
public setLocales(): void { public setLocales(): void {
if (this.options.title) { if (this.options.title && !this.options.header) {
const titleEl = this.header.querySelector('.bim-dialog-title'); const titleEl = this.header.querySelector('.bim-dialog-title');
if (titleEl) { if (titleEl) {
titleEl.textContent = t(this.options.title); titleEl.textContent = t(this.options.title);
@@ -145,20 +148,37 @@ export class BimDialog implements IBimComponent {
header.className = 'bim-dialog-header'; header.className = 'bim-dialog-header';
if (this.options.draggable) header.classList.add('draggable'); if (this.options.draggable) header.classList.add('draggable');
if (this.options.header) {
header.appendChild(this.options.header);
} else {
const title = document.createElement('span'); const title = document.createElement('span');
title.className = 'bim-dialog-title'; title.className = 'bim-dialog-title';
title.textContent = this.options.title ? t(this.options.title) : ''; title.textContent = this.options.title ? t(this.options.title) : '';
header.appendChild(title);
}
if (!this.options.hideCloseButton) {
const closeBtn = document.createElement('span'); const closeBtn = document.createElement('span');
closeBtn.className = 'bim-dialog-close'; closeBtn.className = 'bim-dialog-close';
closeBtn.innerHTML = '&times;';
// 修复 TS 报错:去掉未使用的参数 e if (this.options.closeIcon) {
if (typeof this.options.closeIcon === 'string') {
closeBtn.innerHTML = this.options.closeIcon;
} else {
closeBtn.appendChild(this.options.closeIcon);
}
} else {
closeBtn.innerHTML = `<svg width="24" height="24" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4L4 12M4 4L12 12" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
}
closeBtn.onclick = () => { closeBtn.onclick = () => {
this.close(); this.close();
}; };
header.appendChild(title);
header.appendChild(closeBtn); header.appendChild(closeBtn);
}
// 创建内容区域 (Content) // 创建内容区域 (Content)
const content = document.createElement('div'); const content = document.createElement('div');

View File

@@ -32,7 +32,7 @@ export interface DialogColors {
export interface DialogOptions extends DialogColors { export interface DialogOptions extends DialogColors {
/** 弹窗挂载的父容器 */ /** 弹窗挂载的父容器 */
container: HTMLElement; container: HTMLElement;
/** 弹窗标题 */ /** 弹窗标题(使用默认标题栏时生效,支持国际化 key */
title?: string; title?: string;
/** 弹窗内容,支持 HTML 字符串或 HTMLElement */ /** 弹窗内容,支持 HTML 字符串或 HTMLElement */
content?: HTMLElement | string; content?: HTMLElement | string;
@@ -58,4 +58,15 @@ export interface DialogOptions extends DialogColors {
onOpen?: () => void; onOpen?: () => void;
/** 弹窗唯一标识 ID (可选) */ /** 弹窗唯一标识 ID (可选) */
id?: string; id?: string;
/**
* 自定义标题栏内容(传入 HTMLElement 时,完全替换默认标题栏)。
* 可在此插入 tab、图标、图片等任意内容。
* 传入后 title 字段不再生效。
*/
header?: HTMLElement;
/** 是否隐藏关闭按钮(默认 false */
hideCloseButton?: boolean;
/** 自定义关闭按钮图标(传入 SVG 字符串或 HTMLElement */
closeIcon?: HTMLElement | string;
} }

View File

@@ -0,0 +1,100 @@
.engine-view-dropdown-trigger {
position: absolute;
top: 50px;
right: 95px;
z-index: 999;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #fff;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
transition: filter 0.2s ease;
background: none;
border: none;
padding: 0;
}
.engine-info-button {
position: absolute;
bottom: 20px;
left: 10px;
z-index: 999;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #fff;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
transition: filter 0.2s ease;
background: none;
border: none;
padding: 0;
}
.engine-info-button:hover {
filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.5));
}
.engine-info-button svg {
width: 24px;
height: 24px;
}
.engine-view-dropdown-trigger:hover {
filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.5));
}
.engine-view-dropdown-trigger svg {
width: 24px;
height: 24px;
}
.engine-view-dropdown-menu {
background: var(--bim-panel-bg, rgba(255, 255, 255, 0.95));
border: 1px solid var(--bim-panel-border, rgba(226, 232, 240, 0.6));
border-radius: 8px;
box-shadow: var(--bim-shadow-lg, 0 10px 25px rgba(0, 0, 0, 0.15));
min-width: 160px;
backdrop-filter: blur(12px);
animation: dropdownIn 0.15s ease-out;
}
.engine-view-dropdown-item {
padding: 2px 4px;
cursor: pointer;
font-size: 12px;
color: var(--bim-text-primary, #0f172a);
transition: all 0.15s ease;
line-height: 1.5;
white-space: nowrap;
}
.engine-view-dropdown-item:hover {
background: var(--bim-component-bg-hover, rgba(15, 23, 42, 0.04));
color: var(--bim-primary, #2563eb);
}
.engine-view-dropdown-item:active {
background: var(--bim-component-bg-active, rgba(15, 23, 42, 0.08));
}
.engine-view-dropdown-item:not(:last-child) {
border-bottom: 1px solid var(--bim-panel-border, rgba(226, 232, 240, 0.6));
}
@keyframes dropdownIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -1,3 +1,4 @@
import './index.css';
import type { ThemeConfig } from '../../themes/types'; import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component'; import { IBimComponent } from '../../types/component';
import { themeManager } from '../../services/theme'; import { themeManager } from '../../services/theme';
@@ -9,11 +10,18 @@ import type {
EngineSettingsPatch, EngineSettingsPatch,
EngineSettingPreset, EngineSettingPreset,
SettingPresetLists, SettingPresetLists,
ViewItem,
ViewTreeData,
ModelViewTree,
} from './types'; } from './types';
import { type MeasureMode, type ClearHeightDirection, type ClearHeightSelectType } from '../../types/measure'; import { type MeasureMode, type ClearHeightDirection, type ClearHeightSelectType } from '../../types/measure';
import type { MeasureUnit, MeasurePrecision } from '../measure-panel/types'; import type { MeasureUnit, MeasurePrecision } from '../measure-panel/types';
import type { SectionBoxRange } from '../section-box-panel/types'; import type { SectionBoxRange } from '../section-box-panel/types';
import type { ManagerRegistry } from '../../core/manager-registry'; import type { ManagerRegistry } from '../../core/manager-registry';
import { getIcon } from '../../utils/icon-manager';
import { HelpPanel } from '../help-panel';
import { localeManager } from '../../services/locale';
import type { DrawingPinRecord } from '../../types/events';
// 导入第三方 SDK 的 createEngine 函数(从 npm 包引入) // 导入第三方 SDK 的 createEngine 函数(从 npm 包引入)
import { createEngine as createEngineSDK } from 'iflow-engine-base'; import { createEngine as createEngineSDK } from 'iflow-engine-base';
@@ -25,6 +33,9 @@ export type {
EngineSettingsPatch, EngineSettingsPatch,
EngineSettingPreset, EngineSettingPreset,
SettingPresetLists, SettingPresetLists,
ViewItem,
ViewTreeData,
ModelViewTree,
}; };
/** /**
@@ -65,6 +76,11 @@ export class Engine implements IBimComponent {
private currentSectionMode: 'x' | 'y' | 'z' | 'box' | 'face' | null = null; private currentSectionMode: 'x' | 'y' | 'z' | 'box' | 'face' | null = null;
/** 当前选中的构件信息 */ /** 当前选中的构件信息 */
private selectedComponent: { url: string; id: string } | null = null; private selectedComponent: { url: string; id: string } | null = null;
private viewDropdownTrigger: HTMLElement | null = null;
private viewDropdownMenu: HTMLElement | null = null;
private infoButton: HTMLElement | null = null;
private helpPanel: HelpPanel | null = null;
private unsubscribeLocale: (() => void) | null = null;
/** /**
* 构造函数 * 构造函数
@@ -170,6 +186,10 @@ export class Engine implements IBimComponent {
this.engine.events.on('loading_completed', () => { this.engine.events.on('loading_completed', () => {
console.log('[Engine] 底层 LoadingCompleted 事件触发'); console.log('[Engine] 底层 LoadingCompleted 事件触发');
this.registry.emit('engine:model-loading-completed', {}); this.registry.emit('engine:model-loading-completed', {});
// 首次加载模型后自动显示操作指南(如果用户未点击"不再提醒我"
if (this.helpPanel?.shouldAutoShow()) {
this.helpPanel.show();
}
}); });
} else { } else {
console.warn('[Engine] 底层引擎不支持 events.on 方法,无法监听点击事件'); console.warn('[Engine] 底层引擎不支持 events.on 方法,无法监听点击事件');
@@ -191,6 +211,14 @@ export class Engine implements IBimComponent {
}); });
} }
this.createViewDropdown();
this.initHelpPanel();
this.createInfoButton();
this.unsubscribeLocale = localeManager.subscribe(() => {
this.updateViewDropdownText();
});
} catch (error) { } catch (error) {
console.error('[Engine] Failed to initialize engine:', error); console.error('[Engine] Failed to initialize engine:', error);
this._isInitialized = false; this._isInitialized = false;
@@ -247,6 +275,149 @@ export class Engine implements IBimComponent {
this.engine.viewCube.CameraGoHome(); this.engine.viewCube.CameraGoHome();
} }
public switchToPerspectiveCamera(): void {
if (!this._isInitialized || !this.engine?.cameraModule) {
console.warn('[Engine] Cannot switch to perspective camera: engine not initialized.');
return;
}
this.engine.cameraModule.switchToPerspectiveCamera();
}
public saveMainView(): any {
if (!this._isInitialized || !this.engine?.viewCube) {
console.warn('[Engine] Cannot save main view: engine not initialized.');
return null;
}
const viewData = this.engine.viewCube.saveMainViewPort();
this.registry.emit('view:main-view-saved', { viewData });
return viewData;
}
public restoreMainView(): void {
if (!this._isInitialized || !this.engine?.viewCube) {
console.warn('[Engine] Cannot reset main view: engine not initialized.');
return;
}
this.engine.viewCube.resetMainViewPort();
this.registry.emit('view:main-view-restored', {});
}
public setMainViewPort(viewData: any): void {
if (!this._isInitialized || !this.engine?.viewCube) {
console.warn('[Engine] Cannot set main view: engine not initialized.');
return;
}
this.engine.viewCube.setMainViewPort(viewData);
}
public captureScreenshot(): void {
if (!this._isInitialized || !this.engine?.viewCube) {
console.warn('[Engine] Cannot capture screenshot: engine not initialized.');
return;
}
this.engine.viewCube.screenshot();
}
private createViewDropdown(): void {
const trigger = document.createElement('div');
trigger.className = 'engine-view-dropdown-trigger';
trigger.innerHTML = getIcon('doubleArrowDown');
trigger.title = localeManager.t('viewDropdown.triggerTitle');
trigger.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleViewDropdown();
});
this.container.appendChild(trigger);
this.viewDropdownTrigger = trigger;
}
private initHelpPanel(): void {
this.helpPanel = new HelpPanel();
this.helpPanel.init();
this.container.appendChild(this.helpPanel.element);
}
private createInfoButton(): void {
const button = document.createElement('div');
button.className = 'engine-info-button';
button.innerHTML = getIcon('default');
button.title = '信息';
button.addEventListener('click', (e) => {
e.stopPropagation();
this.helpPanel?.show();
});
this.container.appendChild(button);
this.infoButton = button;
}
private toggleViewDropdown(): void {
if (this.viewDropdownMenu) {
this.closeViewDropdown();
return;
}
const menu = document.createElement('div');
menu.className = 'engine-view-dropdown-menu';
const items = [
{ key: 'switchOrthographic', action: () => this.switchToOrthographicCamera() },
{ key: 'switchPerspective', action: () => this.switchToPerspectiveCamera() },
{ key: 'saveMainView', action: () => this.saveMainView() },
{ key: 'restoreMainView', action: () => this.restoreMainView() },
{ key: 'captureScreenshot', action: () => this.captureScreenshot() },
];
items.forEach(item => {
const row = document.createElement('div');
row.className = 'engine-view-dropdown-item';
row.dataset.key = item.key;
row.textContent = localeManager.t(`viewDropdown.${item.key}`);
row.addEventListener('click', (e) => {
e.stopPropagation();
item.action();
this.closeViewDropdown();
});
menu.appendChild(row);
});
const rect = this.viewDropdownTrigger!.getBoundingClientRect();
menu.style.position = 'fixed';
menu.style.top = `${rect.bottom + 4}px`;
menu.style.right = `${window.innerWidth - rect.right}px`;
menu.style.zIndex = '10000';
document.body.appendChild(menu);
this.viewDropdownMenu = menu;
const closeHandler = (e: MouseEvent) => {
if (!menu.contains(e.target as Node)) {
this.closeViewDropdown();
document.removeEventListener('mousedown', closeHandler);
}
};
document.addEventListener('mousedown', closeHandler);
}
private closeViewDropdown(): void {
if (this.viewDropdownMenu) {
this.viewDropdownMenu.remove();
this.viewDropdownMenu = null;
}
}
private updateViewDropdownText(): void {
if (!this.viewDropdownTrigger) return;
this.viewDropdownTrigger.title = localeManager.t('viewDropdown.triggerTitle');
if (!this.viewDropdownMenu) return;
const items = this.viewDropdownMenu.querySelectorAll('.engine-view-dropdown-item');
const keys = ['switchOrthographic', 'switchPerspective', 'saveMainView', 'restoreMainView', 'captureScreenshot'];
items.forEach((item, index) => {
if (keys[index]) {
item.textContent = localeManager.t(`viewDropdown.${keys[index]}`);
}
});
}
/** /**
* 订阅原始引擎事件 * 订阅原始引擎事件
* 用于需要访问底层引擎事件的场景(如测量回调、剖切移动等) * 用于需要访问底层引擎事件的场景(如测量回调、剖切移动等)
@@ -318,6 +489,48 @@ export class Engine implements IBimComponent {
this.engine.resumeRendering(); this.engine.resumeRendering();
} }
/**
* 创建当前视点快照
* @param callback 底层完成后的回调,参数为视点数据对象
*/
public createDrawingPin(callback: (pin: any) => void): void {
console.log('[Engine] createDrawingPin called');
if (!this._isInitialized || !this.engine?.drawingPin) {
console.warn('[Engine] drawingPin.create not available. initialized:', this._isInitialized, 'drawingPin:', !!this.engine?.drawingPin);
return;
}
console.log('[Engine] calling this.engine.drawingPin.create');
this.engine.drawingPin.create(callback);
}
/**
* 还原视点快照
* @param pin 视点数据对象
*/
public async restoreDrawingPin(pin: any): Promise<void> {
if (!this._isInitialized || !this.engine?.drawingPin) {
console.warn('[Engine] drawingPin.restore not available.');
return;
}
return this.engine.drawingPin.restore(pin,'edit');
}
/**
* 设置图钉记录列表(平铺结构,由组件内部构建树)
* @param records 图钉记录列表
*/
public setPinRecords(records: DrawingPinRecord[]): void {
console.log('[Engine] setPinRecords called, records count:', records.length);
this.registry.emit('drawingPin:list-updated', { records });
}
/**
* 兼容旧命名,内部仍按记录列表处理。
*/
public setPinList(records: DrawingPinRecord[]): void {
this.setPinRecords(records);
}
/** /**
* 销毁 3D 引擎,释放 GPU 资源 * 销毁 3D 引擎,释放 GPU 资源
*/ */
@@ -326,6 +539,23 @@ export class Engine implements IBimComponent {
console.warn('[Engine] Engine not initialized.'); console.warn('[Engine] Engine not initialized.');
return; return;
} }
this.closeViewDropdown();
if (this.viewDropdownTrigger) {
this.viewDropdownTrigger.remove();
this.viewDropdownTrigger = null;
}
if (this.infoButton) {
this.infoButton.remove();
this.infoButton = null;
}
if (this.helpPanel) {
this.helpPanel.destroy();
this.helpPanel = null;
}
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
this.engine.dispose(); this.engine.dispose();
} }
@@ -443,6 +673,11 @@ export class Engine implements IBimComponent {
this.currentMeasureType = null; this.currentMeasureType = null;
} }
public resetMeasureState(): void {
this.currentMeasureType = null;
this.isMeasureActive = false;
}
/** /**
* 清除所有测量标注(不停用测量模式) * 清除所有测量标注(不停用测量模式)
*/ */
@@ -1205,6 +1440,15 @@ export class Engine implements IBimComponent {
return this.engine.setting.getGroundElevation?.() ?? 0; return this.engine.setting.getGroundElevation?.() ?? 0;
} }
/** 切回正交视角 */
public switchToOrthographicCamera() {
if (!this._isInitialized) {
return 0;
}
this.engine.cameraModule.switchToOrthographicCamera()
}
// ==================== 结束:设置功能 ==================== // ==================== 结束:设置功能 ====================
// ==================== 漫游功能 ==================== // ==================== 漫游功能 ====================
@@ -1233,7 +1477,6 @@ export class Engine implements IBimComponent {
console.log('[Engine] Activating first person mode'); console.log('[Engine] Activating first person mode');
this.engine.controlModule.switchFirstPersonMode(); this.engine.controlModule.switchFirstPersonMode();
this.loadWalkSettings();
this.isWalkModeActive = true; this.isWalkModeActive = true;
} }
@@ -1250,29 +1493,23 @@ export class Engine implements IBimComponent {
return; return;
} }
console.log('[Engine] Deactivating first person mode');
this.engine.controlModule.switchDefaultMode(); this.engine.controlModule.switchDefaultMode();
console.log('[Engine] Deactivating first person mode');
this.isWalkModeActive = false; this.isWalkModeActive = false;
} }
private static WALK_SPEED_KEY = 'bim-walk-speed';
private static WALK_GRAVITY_KEY = 'bim-walk-gravity';
private static WALK_COLLISION_KEY = 'bim-walk-collision';
/** /**
* 获取漫游移动速度 * 获取漫游移动速度
* @returns 当前移动速度值 * @returns 当前移动速度值
*/ */
public getWalkSpeed(): number { public getWalkSpeed(): number {
if (!this._isInitialized || !this.engine?.controlModule) { if (!this._isInitialized) {
console.error('[Engine] Cannot get walk speed: engine not initialized.'); console.error('[Engine] Cannot get walk speed: engine not initialized.');
return 1; return 1;
} }
if (typeof this.engine.controlModule.getMoveSpeed !== 'function') { const speed = this.engine.controlModule.getMoveSpeed();
console.warn('[Engine] controlModule.getMoveSpeed not found, fallback to 1'); console.log('[Engine] getWalkSpeed from controlModule:', speed);
return 1; return speed;
}
return this.engine.controlModule.getMoveSpeed();
} }
/** /**
@@ -1284,23 +1521,49 @@ export class Engine implements IBimComponent {
console.error('[Engine] Cannot set walk speed: engine not initialized.'); console.error('[Engine] Cannot set walk speed: engine not initialized.');
return; return;
} }
localStorage.setItem(Engine.WALK_SPEED_KEY, String(speed));
this.engine.controlModule.setMoveSpeed(speed); this.engine.controlModule.setMoveSpeed(speed);
} }
/**
* 获取漫游重力开关状态
* @returns 是否启用重力
*/
public getWalkGravity(): boolean {
if (!this._isInitialized) {
console.error('[Engine] Cannot get walk gravity: engine not initialized.');
return false;
}
const gravity = this.engine.controlModule.getApplyGravity();
console.log('[Engine] getWalkGravity from controlModule:', gravity);
return gravity;
}
/** /**
* 设置漫游重力开关 * 设置漫游重力开关
* @param enabled 是否启用重力 * @param enabled 是否启用重力
*/ */
public setWalkGravity(enabled: boolean): void { public setWalkGravity(enabled: boolean): void {
if (!this._isInitialized || !this.engine?.controlModule) { if (!this._isInitialized) {
console.error('[Engine] Cannot set walk gravity: engine not initialized.'); console.error('[Engine] Cannot set walk gravity: engine not initialized.');
return; return;
} }
localStorage.setItem(Engine.WALK_GRAVITY_KEY, String(enabled));
this.engine.controlModule.setApplyGravity(enabled); this.engine.controlModule.setApplyGravity(enabled);
} }
/**
* 获取漫游碰撞检测开关状态
* @returns 是否启用碰撞检测
*/
public getWalkCollision(): boolean {
if (!this._isInitialized) {
console.error('[Engine] Cannot get walk collision: engine not initialized.');
return false;
}
const collision = this.engine.controlModule.getApplyCollision();
console.log('[Engine] getWalkCollision from controlModule:', collision);
return collision;
}
/** /**
* 设置漫游碰撞检测开关 * 设置漫游碰撞检测开关
* @param enabled 是否启用碰撞检测 * @param enabled 是否启用碰撞检测
@@ -1310,22 +1573,9 @@ export class Engine implements IBimComponent {
console.error('[Engine] Cannot set walk collision: engine not initialized.'); console.error('[Engine] Cannot set walk collision: engine not initialized.');
return; return;
} }
localStorage.setItem(Engine.WALK_COLLISION_KEY, String(enabled));
this.engine.controlModule.setApplyCollision(enabled); this.engine.controlModule.setApplyCollision(enabled);
} }
private loadWalkSettings(): void {
if (!this.engine?.controlModule) return;
const speed = localStorage.getItem(Engine.WALK_SPEED_KEY);
const gravity = localStorage.getItem(Engine.WALK_GRAVITY_KEY);
const collision = localStorage.getItem(Engine.WALK_COLLISION_KEY);
this.engine.controlModule.setMoveSpeed(speed ? Number(speed) : 1);
this.engine.controlModule.setApplyGravity(gravity === 'true');
this.engine.controlModule.setApplyCollision(collision === 'true');
}
/** /**
* 切换小地图显示状态 * 切换小地图显示状态
*/ */
@@ -1482,6 +1732,34 @@ export class Engine implements IBimComponent {
return this.engine.modelTree.getMajorTreeData(); return this.engine.modelTree.getMajorTreeData();
} }
/**
* 获取所有已加载模型的视图树数据
* @returns 模型视图树数组
*/
public getAllViewTreeData(): ModelViewTree[] {
if (!this._isInitialized || !this.engine?.viewTree) {
return [];
}
return this.engine.viewTree.getAllViewTreeData();
}
/**
* 打开指定模型的指定视图
* @param url 模型文件地址
* @param viewId 视图 ID
*/
public openView(url: string, viewId: string): void {
if (!this._isInitialized || !this.engine?.viewTree) {
console.warn('[Engine] Cannot open view: engine not initialized or viewTree not available.');
return;
}
try {
this.engine.viewTree.openView(url, viewId);
} catch (e) {
console.warn('[Engine] openView failed:', e);
}
}
// ==================== 结束:模型树 ==================== // ==================== 结束:模型树 ====================
/** /**

View File

@@ -107,3 +107,25 @@ export interface SettingPresetLists {
hdr: PresetListItem[]; hdr: PresetListItem[];
sky: PresetListItem[]; sky: PresetListItem[];
} }
/** 视图项 */
export interface ViewItem {
id: string;
name: string;
url: string;
}
/** 视图树数据 */
export interface ViewTreeData {
ceilingPlans: ViewItem[];
elevations: ViewItem[];
floorPlans: ViewItem[];
sections: ViewItem[];
}
/** 模型视图树 */
export interface ModelViewTree {
name: string;
url: string;
views: ViewTreeData;
}

View File

@@ -0,0 +1,344 @@
.bim-help-panel-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s ease;
}
.bim-help-panel-overlay.active {
visibility: visible;
opacity: 1;
}
.bim-help-panel-container {
background: var(--bim-panel-bg);
width: 720px;
max-width: 92%;
max-height: 85vh;
border-radius: 10px;
box-shadow: var(--bim-shadow-lg);
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--bim-panel-border);
}
.bim-help-panel-close {
position: absolute;
right: 16px;
top: 12px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
cursor: pointer;
color: var(--bim-text-tertiary);
background: transparent;
border: none;
line-height: 1;
z-index: 10;
transition: color 0.15s ease;
}
.bim-help-panel-close:hover {
color: var(--bim-text-primary);
}
.bim-help-panel-header {
display: flex;
justify-content: center;
padding: 16px 20px;
gap: 8px;
border-bottom: 1px solid var(--bim-divider);
background: var(--bim-panel-header-bg);
flex-shrink: 0;
}
.bim-help-panel-tab-btn {
background: var(--bim-component-bg);
border: 1px solid var(--bim-border-default);
padding: 8px 28px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
border-radius: 5px;
color: var(--bim-text-secondary);
}
.bim-help-panel-tab-btn:hover {
border-color: var(--bim-border-strong);
color: var(--bim-text-primary);
}
.bim-help-panel-tab-btn.active {
border: 2px solid var(--bim-text-primary);
color: var(--bim-text-primary);
background: var(--bim-component-bg);
}
.bim-help-panel-body {
padding: 20px 24px;
overflow-y: auto;
flex: 1;
}
.bim-help-panel-body::-webkit-scrollbar {
width: 5px;
}
.bim-help-panel-body::-webkit-scrollbar-track {
background: transparent;
}
.bim-help-panel-body::-webkit-scrollbar-thumb {
background: var(--bim-scrollbar-thumb);
border-radius: 3px;
}
.bim-help-panel-tab-content {
display: none;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.bim-help-panel-tab-content.active {
display: grid;
}
.bim-help-panel-card {
background: var(--bim-component-bg);
border: 1px solid var(--bim-border-subtle);
border-radius: 8px;
padding: 16px 14px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 110px;
gap: 10px;
}
.bim-help-panel-card-span-2 {
grid-column: span 2;
}
.bim-help-panel-card-title {
font-size: 13px;
color: var(--bim-text-primary);
font-weight: 700;
width: 100%;
text-align: left;
border-left: 3px solid var(--bim-primary);
padding-left: 8px;
}
.bim-help-panel-control-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
color: var(--bim-text-primary);
font-size: 13px;
}
.bim-help-panel-kbd {
background: var(--bim-bg-base);
border: 1px solid var(--bim-border-default);
border-radius: 4px;
box-shadow: 0 2px 0 var(--bim-border-subtle);
color: var(--bim-text-primary);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
padding: 4px 10px;
min-width: 24px;
height: 26px;
font-family: ui-monospace, SFMono-Regular, monospace;
}
.bim-help-panel-wasd-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.bim-help-panel-kbd-row {
display: flex;
gap: 4px;
}
.bim-help-panel-rule-desc {
font-size: 12px;
color: var(--bim-text-secondary);
line-height: 1.5;
text-align: center;
}
.bim-help-panel-bold {
color: var(--bim-text-primary);
font-weight: 700;
}
.bim-help-panel-mouse-box {
width: 22px;
height: 32px;
border: 2px solid var(--bim-text-primary);
border-radius: 8px;
position: relative;
background: var(--bim-bg-base);
flex-shrink: 0;
}
.bim-help-panel-mouse-box::after {
content: '';
position: absolute;
top: 4px;
left: 50%;
height: 10px;
border-right: 2px solid var(--bim-text-primary);
transform: translateX(-50%);
}
.bim-help-panel-mouse-left {
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 55%;
border-radius: 6px 0 0 0;
background: var(--bim-text-primary);
}
.bim-help-panel-mouse-right {
position: absolute;
top: 0;
right: 0;
width: 50%;
height: 55%;
border-radius: 0 6px 0 0;
background: var(--bim-text-primary);
}
.bim-help-panel-mouse-wheel {
position: absolute;
top: 6px;
left: 50%;
width: 4px;
height: 8px;
background: var(--bim-text-primary);
border-radius: 2px;
transform: translateX(-50%);
z-index: 2;
}
.bim-help-panel-teleport-icon {
width: 28px;
height: 28px;
border: 2px solid var(--bim-text-primary);
border-radius: 50%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.bim-help-panel-teleport-icon::before,
.bim-help-panel-teleport-icon::after {
content: '';
position: absolute;
background: var(--bim-text-primary);
}
.bim-help-panel-teleport-icon::before {
width: 36px;
height: 2px;
}
.bim-help-panel-teleport-icon::after {
width: 2px;
height: 36px;
}
.bim-help-panel-teleport-inner {
width: 6px;
height: 6px;
background: var(--bim-text-primary);
border-radius: 50%;
z-index: 2;
}
.bim-help-panel-footer {
display: flex;
justify-content: center;
gap: 16px;
padding: 16px 24px 24px;
border-top: 1px solid var(--bim-divider);
flex-shrink: 0;
}
.bim-help-panel-btn {
padding: 10px 48px;
border-radius: 6px;
border: none;
font-size: 14px;
cursor: pointer;
font-weight: 600;
transition: all 0.15s ease;
}
.bim-help-panel-btn-primary {
background: var(--bim-primary);
color: var(--bim-text-inverse);
}
.bim-help-panel-btn-primary:hover {
background: var(--bim-primary-hover);
}
.bim-help-panel-btn-secondary {
background: var(--bim-component-bg-hover);
color: var(--bim-text-secondary);
border: 1px solid var(--bim-border-default);
}
.bim-help-panel-btn-secondary:hover {
background: var(--bim-component-bg-active);
color: var(--bim-text-primary);
}
@media (max-width: 640px) {
.bim-help-panel-container {
width: 96%;
max-height: 90vh;
}
.bim-help-panel-tab-content {
grid-template-columns: 1fr;
}
.bim-help-panel-card-span-2 {
grid-column: span 1;
}
.bim-help-panel-tab-btn {
padding: 8px 20px;
font-size: 13px;
}
.bim-help-panel-btn {
padding: 10px 24px;
}
}

View File

@@ -0,0 +1,452 @@
import './index.css';
import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { HelpPanelOptions, HelpTab } from './types';
export class HelpPanel implements IBimComponent {
public element!: HTMLElement;
private overlay!: HTMLElement;
private container!: HTMLElement;
private options: HelpPanelOptions;
private viewTabBtn!: HTMLButtonElement;
private fpsTabBtn!: HTMLButtonElement;
private viewContent!: HTMLElement;
private fpsContent!: HTMLElement;
private confirmBtn!: HTMLButtonElement;
private dontRemindBtn!: HTMLButtonElement;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
private static readonly STORAGE_KEY = 'bim-engine:help-panel:hidden';
constructor(options: HelpPanelOptions = {}) {
this.options = options;
}
public init(): void {
this.element = this.createPanel();
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales();
this.setTheme(themeManager.getTheme());
}
public show(): void {
this.overlay.classList.add('active');
}
public hide(): void {
this.overlay.classList.remove('active');
}
public isVisible(): boolean {
return this.overlay.classList.contains('active');
}
public shouldAutoShow(): boolean {
const storageKey = this.options.storageKey ?? HelpPanel.STORAGE_KEY;
return localStorage.getItem(storageKey) !== 'true';
}
private createPanel(): HTMLElement {
const wrapper = document.createElement('div');
wrapper.className = 'bim-help-panel-wrapper';
this.overlay = document.createElement('div');
this.overlay.className = 'bim-help-panel-overlay';
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) {
this.hide();
}
});
this.container = document.createElement('div');
this.container.className = 'bim-help-panel-container';
const header = this.createHeader();
const body = this.createBody();
const footer = this.createFooter();
this.container.appendChild(header);
this.container.appendChild(body);
this.container.appendChild(footer);
this.overlay.appendChild(this.container);
wrapper.appendChild(this.overlay);
return wrapper;
}
private createHeader(): HTMLElement {
const header = document.createElement('div');
header.className = 'bim-help-panel-header';
this.viewTabBtn = document.createElement('button');
this.viewTabBtn.className = 'bim-help-panel-tab-btn active';
this.viewTabBtn.addEventListener('click', () => this.switchTab('view'));
this.fpsTabBtn = document.createElement('button');
this.fpsTabBtn.className = 'bim-help-panel-tab-btn';
this.fpsTabBtn.addEventListener('click', () => this.switchTab('fps'));
header.appendChild(this.viewTabBtn);
header.appendChild(this.fpsTabBtn);
return header;
}
private createBody(): HTMLElement {
const body = document.createElement('div');
body.className = 'bim-help-panel-body';
this.viewContent = this.createViewTabContent();
this.fpsContent = this.createFpsTabContent();
body.appendChild(this.viewContent);
body.appendChild(this.fpsContent);
return body;
}
private createViewTabContent(): HTMLElement {
const content = document.createElement('div');
content.className = 'bim-help-panel-tab-content active';
content.dataset.tab = 'view';
// 旋转视图 - 带鼠标左键图标
content.appendChild(this.createMouseCard('rotateView', 'left'));
// 平移视图 - 带鼠标右键图标
content.appendChild(this.createMouseCard('panView', 'right'));
// 缩放视图 - 带鼠标滚轮图标
content.appendChild(this.createMouseCard('zoomView', 'wheel'));
// 框选构件
const boxSelectCard = this.createCard('boxSelect', 'ctrlLeftDrag');
content.appendChild(boxSelectCard);
// 构件选择 - 占2列
const multiSelectCard = this.createMultiSelectCard();
content.appendChild(multiSelectCard);
// 视图复位
const resetCard = this.createCard('resetView', 'doubleClickBlankShort');
content.appendChild(resetCard);
// 旋转点规则 - 占2列
const ruleCard = this.createRuleCard();
content.appendChild(ruleCard);
return content;
}
private createMouseCard(titleKey: string, type: 'left' | 'right' | 'wheel'): HTMLElement {
const card = document.createElement('div');
card.className = 'bim-help-panel-card';
const title = document.createElement('div');
title.className = 'bim-help-panel-card-title';
title.dataset.localeKey = `help.${titleKey}`;
const iconWrapper = document.createElement('div');
iconWrapper.className = 'bim-help-panel-control-row';
const mouseBox = document.createElement('div');
mouseBox.className = 'bim-help-panel-mouse-box';
if (type === 'left') {
const leftBtn = document.createElement('div');
leftBtn.className = 'bim-help-panel-mouse-left';
mouseBox.appendChild(leftBtn);
} else if (type === 'right') {
const rightBtn = document.createElement('div');
rightBtn.className = 'bim-help-panel-mouse-right';
mouseBox.appendChild(rightBtn);
} else if (type === 'wheel') {
const wheel = document.createElement('div');
wheel.className = 'bim-help-panel-mouse-wheel';
mouseBox.appendChild(wheel);
}
iconWrapper.appendChild(mouseBox);
const desc = document.createElement('div');
desc.className = 'bim-help-panel-rule-desc';
desc.dataset.localeKey = `help.${type === 'left' ? 'mouseLeftDragShort' : type === 'right' ? 'mouseRightDragShort' : 'mouseWheelShort'}`;
card.appendChild(title);
card.appendChild(iconWrapper);
card.appendChild(desc);
return card;
}
private createMultiSelectCard(): HTMLElement {
const card = document.createElement('div');
card.className = 'bim-help-panel-card bim-help-panel-card-span-2';
const title = document.createElement('div');
title.className = 'bim-help-panel-card-title';
title.dataset.localeKey = 'help.componentSelect';
const row = document.createElement('div');
row.className = 'bim-help-panel-control-row';
row.style.gap = '40px';
row.innerHTML = `
<div style="text-align:center;">
<div class="bim-help-panel-control-row"><kbd class="bim-help-panel-kbd">Ctrl</kbd> <span class="bim-help-panel-bold">+</span> <span class="bim-help-panel-bold">${t('help.click')}</span></div>
<div class="bim-help-panel-rule-desc" data-locale-key="help.addSelect">${t('help.addSelect')}</div>
</div>
<div style="text-align:center;">
<div class="bim-help-panel-control-row"><kbd class="bim-help-panel-kbd">Shift</kbd> <span class="bim-help-panel-bold">+</span> <span class="bim-help-panel-bold">${t('help.click')}</span></div>
<div class="bim-help-panel-rule-desc" data-locale-key="help.removeSelect">${t('help.removeSelect')}</div>
</div>
`;
card.appendChild(title);
card.appendChild(row);
return card;
}
private createRuleCard(): HTMLElement {
const card = document.createElement('div');
card.className = 'bim-help-panel-card bim-help-panel-card-span-2';
const title = document.createElement('div');
title.className = 'bim-help-panel-card-title';
title.dataset.localeKey = 'help.rotateRuleTitle';
const desc = document.createElement('div');
desc.className = 'bim-help-panel-rule-desc';
desc.style.textAlign = 'left';
desc.style.width = '100%';
desc.innerHTML = `
<div style="margin-bottom:6px;"><span class="bim-help-panel-bold">• ${t('help.pointToComponent')}</span> ${t('help.pointToComponentDesc')}</div>
<div><span class="bim-help-panel-bold">• ${t('help.pointToBlank')}</span> ${t('help.pointToBlankDesc')}</div>
`;
card.appendChild(title);
card.appendChild(desc);
return card;
}
private createFpsTabContent(): HTMLElement {
const content = document.createElement('div');
content.className = 'bim-help-panel-tab-content';
content.dataset.tab = 'fps';
// 漫游移动 - 占2列
const roamCard = document.createElement('div');
roamCard.className = 'bim-help-panel-card bim-help-panel-card-span-2';
const roamTitle = document.createElement('div');
roamTitle.className = 'bim-help-panel-card-title';
roamTitle.dataset.localeKey = 'help.roamMove';
const roamContent = document.createElement('div');
roamContent.className = 'bim-help-panel-control-row';
roamContent.style.gap = '48px';
roamContent.innerHTML = `
<div class="bim-help-panel-wasd-group">
<div class="bim-help-panel-kbd-row"><kbd class="bim-help-panel-kbd">W</kbd></div>
<div class="bim-help-panel-kbd-row">
<kbd class="bim-help-panel-kbd">A</kbd>
<kbd class="bim-help-panel-kbd">S</kbd>
<kbd class="bim-help-panel-kbd">D</kbd>
</div>
</div>
<span style="color:var(--bim-text-tertiary);font-weight:100;">|</span>
<div class="bim-help-panel-wasd-group">
<div class="bim-help-panel-kbd-row"><kbd class="bim-help-panel-kbd">▲</kbd></div>
<div class="bim-help-panel-kbd-row">
<kbd class="bim-help-panel-kbd">◄</kbd>
<kbd class="bim-help-panel-kbd">▼</kbd>
<kbd class="bim-help-panel-kbd">►</kbd>
</div>
</div>
`;
const roamDesc = document.createElement('div');
roamDesc.className = 'bim-help-panel-rule-desc';
roamDesc.dataset.localeKey = 'help.roamMoveDesc';
roamCard.appendChild(roamTitle);
roamCard.appendChild(roamContent);
roamCard.appendChild(roamDesc);
content.appendChild(roamCard);
// 加速移动
content.appendChild(this.createCard('speedUp', 'shiftArrow'));
// 升降高度
content.appendChild(this.createCard('upDown', 'qeKey', 'landOnFloor'));
// 环视四周 - 带鼠标图标
const lookCard = document.createElement('div');
lookCard.className = 'bim-help-panel-card';
const lookTitle = document.createElement('div');
lookTitle.className = 'bim-help-panel-card-title';
lookTitle.dataset.localeKey = 'help.lookAround';
const lookIcon = document.createElement('div');
lookIcon.className = 'bim-help-panel-control-row';
const lookMouse = document.createElement('div');
lookMouse.className = 'bim-help-panel-mouse-box';
const lookLeft = document.createElement('div');
lookLeft.className = 'bim-help-panel-mouse-left';
lookMouse.appendChild(lookLeft);
lookIcon.appendChild(lookMouse);
const lookDesc = document.createElement('div');
lookDesc.className = 'bim-help-panel-rule-desc';
lookDesc.dataset.localeKey = 'help.lookAroundDesc';
lookCard.appendChild(lookTitle);
lookCard.appendChild(lookIcon);
lookCard.appendChild(lookDesc);
content.appendChild(lookCard);
// 位置传送 - 带传送图标
const tpCard = document.createElement('div');
tpCard.className = 'bim-help-panel-card';
const tpTitle = document.createElement('div');
tpTitle.className = 'bim-help-panel-card-title';
tpTitle.dataset.localeKey = 'help.teleport';
const tpIcon = document.createElement('div');
tpIcon.className = 'bim-help-panel-control-row';
const tpIconInner = document.createElement('div');
tpIconInner.className = 'bim-help-panel-teleport-icon';
const tpInnerDot = document.createElement('div');
tpInnerDot.className = 'bim-help-panel-teleport-inner';
tpIconInner.appendChild(tpInnerDot);
tpIcon.appendChild(tpIconInner);
const tpDesc = document.createElement('div');
tpDesc.className = 'bim-help-panel-rule-desc';
tpDesc.dataset.localeKey = 'help.teleportDesc';
tpCard.appendChild(tpTitle);
tpCard.appendChild(tpIcon);
tpCard.appendChild(tpDesc);
content.appendChild(tpCard);
return content;
}
private createCard(titleKey: string, contentKey: string, descKey?: string): HTMLElement {
const card = document.createElement('div');
card.className = 'bim-help-panel-card';
const title = document.createElement('div');
title.className = 'bim-help-panel-card-title';
title.dataset.localeKey = `help.${titleKey}`;
const content = document.createElement('div');
content.className = 'bim-help-panel-control-row';
content.dataset.localeKey = `help.${contentKey}`;
card.appendChild(title);
card.appendChild(content);
if (descKey) {
const desc = document.createElement('div');
desc.className = 'bim-help-panel-rule-desc';
desc.dataset.localeKey = `help.${descKey}`;
card.appendChild(desc);
}
return card;
}
private createFooter(): HTMLElement {
const footer = document.createElement('div');
footer.className = 'bim-help-panel-footer';
this.confirmBtn = document.createElement('button');
this.confirmBtn.className = 'bim-help-panel-btn bim-help-panel-btn-primary';
this.confirmBtn.addEventListener('click', () => this.hide());
this.dontRemindBtn = document.createElement('button');
this.dontRemindBtn.className = 'bim-help-panel-btn bim-help-panel-btn-secondary';
this.dontRemindBtn.addEventListener('click', () => this.dontRemind());
footer.appendChild(this.confirmBtn);
footer.appendChild(this.dontRemindBtn);
return footer;
}
private switchTab(tab: HelpTab): void {
this.viewTabBtn.classList.toggle('active', tab === 'view');
this.fpsTabBtn.classList.toggle('active', tab === 'fps');
this.viewContent.classList.toggle('active', tab === 'view');
this.fpsContent.classList.toggle('active', tab === 'fps');
}
private dontRemind(): void {
const storageKey = this.options.storageKey ?? HelpPanel.STORAGE_KEY;
localStorage.setItem(storageKey, 'true');
this.hide();
}
public setLocales(): void {
this.viewTabBtn.textContent = t('help.engineControl');
this.fpsTabBtn.textContent = t('help.fpsControl');
this.confirmBtn.textContent = t('help.gotIt');
this.dontRemindBtn.textContent = t('help.dontRemind');
this.element.querySelectorAll('[data-locale-key]').forEach((el) => {
const key = el.getAttribute('data-locale-key');
if (key) {
el.textContent = t(key);
}
});
}
public setTheme(theme: ThemeConfig): void {
this.element.style.setProperty('--bim-bg-overlay', 'rgba(0, 0, 0, 0.5)');
this.element.style.setProperty('--bim-panel-bg', theme.bgElevated);
this.element.style.setProperty('--bim-panel-border', theme.panelBorder);
this.element.style.setProperty('--bim-shadow-lg', theme.shadowLg);
this.element.style.setProperty('--bim-text-primary', theme.textPrimary);
this.element.style.setProperty('--bim-text-secondary', theme.textSecondary);
this.element.style.setProperty('--bim-text-tertiary', theme.textTertiary);
this.element.style.setProperty('--bim-primary', theme.primary);
this.element.style.setProperty('--bim-primary-hover', theme.primaryHover);
this.element.style.setProperty('--bim-text-inverse', theme.textInverse);
this.element.style.setProperty('--bim-divider', theme.divider);
this.element.style.setProperty('--bim-border-default', theme.borderDefault);
this.element.style.setProperty('--bim-border-subtle', theme.borderSubtle);
this.element.style.setProperty('--bim-component-bg', theme.componentBg);
this.element.style.setProperty('--bim-component-bg-hover', theme.componentBgHover);
this.element.style.setProperty('--bim-bg-inset', theme.bgInset);
this.element.style.setProperty('--bim-scrollbar-thumb', theme.scrollbarThumb);
this.element.style.setProperty('--bim-scrollbar-thumb-hover', theme.scrollbarThumbHover);
}
public destroy(): void {
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
if (this.overlay) {
this.overlay.remove();
}
}
}

View File

@@ -0,0 +1,31 @@
/**
* 操作指引面板选项
*/
export interface HelpPanelOptions {
/** 是否默认隐藏提醒标记 */
hideRemind?: boolean;
/** 自定义存储键名 */
storageKey?: string;
}
/**
* 标签页类型
*/
export type HelpTab = 'view' | 'fps';
/**
* 操作指南内容项
*/
export interface GuideItem {
title: string;
content: string;
fullWidth?: boolean;
}
/**
* 面板事件处理器
*/
export interface HelpPanelHandlers {
onClose?: () => void;
onDontRemind?: () => void;
}

View File

@@ -50,7 +50,7 @@ export class MeasureDockPanel implements IBimComponent {
private readonly settingsSaveBtn: HTMLButtonElement; private readonly settingsSaveBtn: HTMLButtonElement;
private readonly settingsBackBtn: HTMLButtonElement; private readonly settingsBackBtn: HTMLButtonElement;
private activeMode: MeasureMode; private activeMode: MeasureMode | null;
private isExpanded: boolean; private isExpanded: boolean;
private clearHeightDirection: ClearHeightDirection; private clearHeightDirection: ClearHeightDirection;
private clearHeightSelectType: ClearHeightSelectType; private clearHeightSelectType: ClearHeightSelectType;
@@ -108,7 +108,7 @@ export class MeasureDockPanel implements IBimComponent {
this.applyExpandedState(); this.applyExpandedState();
this.applyClearHeightOptionsState(); this.applyClearHeightOptionsState();
this.applyViewState(); this.applyViewState();
this.syncActiveMode(this.activeMode); this.syncActiveMode(this.activeMode ?? 'distance');
} }
public setTheme(theme: ThemeConfig): void { public setTheme(theme: ThemeConfig): void {
@@ -160,6 +160,7 @@ export class MeasureDockPanel implements IBimComponent {
} }
public switchMode(mode: MeasureMode, triggerCallback: boolean = true): void { public switchMode(mode: MeasureMode, triggerCallback: boolean = true): void {
if (this.activeMode === mode) return;
this.activeMode = mode; this.activeMode = mode;
this.syncActiveMode(mode); this.syncActiveMode(mode);
this.applyClearHeightOptionsState(); this.applyClearHeightOptionsState();
@@ -579,4 +580,11 @@ export class MeasureDockPanel implements IBimComponent {
button.classList.toggle('is-active', key === mode); button.classList.toggle('is-active', key === mode);
} }
} }
public clearActiveMode(): void {
for (const button of this.modeButtons.values()) {
button.classList.remove('is-active');
}
this.activeMode = null;
}
} }

View File

@@ -23,7 +23,7 @@ export class MeasurePanel implements IBimComponent {
public element: HTMLElement; public element: HTMLElement;
private options: MeasurePanelOptions; private options: MeasurePanelOptions;
private activeMode: MeasureMode; private activeMode: MeasureMode | null;
private isExpanded: boolean; private isExpanded: boolean;
private result: MeasureResult | null = null; private result: MeasureResult | null = null;
@@ -129,7 +129,7 @@ export class MeasurePanel implements IBimComponent {
this.renderResult(); this.renderResult();
// 触发初始测量模式的回调(让外部知道默认激活了哪个模式) // 触发初始测量模式的回调(让外部知道默认激活了哪个模式)
if (this.options.onModeChange) { if (this.options.onModeChange && this.activeMode) {
this.options.onModeChange(this.activeMode); this.options.onModeChange(this.activeMode);
} }
} }
@@ -187,7 +187,9 @@ export class MeasurePanel implements IBimComponent {
this.settingsBtn.setAttribute('aria-label', this.settingsBtn.title); this.settingsBtn.setAttribute('aria-label', this.settingsBtn.title);
// 4) 主值 label随模式变化 // 4) 主值 label随模式变化
if (this.activeMode) {
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode)); this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
}
// 6) XYZ label使用 key // 6) XYZ label使用 key
// 这里 label 在 createDom 已经是固定文本节点,直接用 setText 更新更直观 // 这里 label 在 createDom 已经是固定文本节点,直接用 setText 更新更直观
@@ -231,7 +233,7 @@ export class MeasurePanel implements IBimComponent {
/** /**
* 获取当前测量方式 * 获取当前测量方式
*/ */
public getActiveMode(): MeasureMode { public getActiveMode(): MeasureMode | null {
return this.activeMode; return this.activeMode;
} }
@@ -875,7 +877,7 @@ export class MeasurePanel implements IBimComponent {
} }
/** /**
* 应用当前选中按钮样式 * 应用"当前选中按钮"样式
*/ */
private applyActiveModeState(): void { private applyActiveModeState(): void {
for (const [mode, btn] of this.toolButtons.entries()) { for (const [mode, btn] of this.toolButtons.entries()) {
@@ -887,6 +889,13 @@ export class MeasurePanel implements IBimComponent {
} }
} }
public clearActiveMode(): void {
for (const btn of this.toolButtons.values()) {
btn.classList.remove('is-active');
}
this.activeMode = null;
}
/** /**
* 渲染结果区(根据 activeMode 从 result 里取对应字段) * 渲染结果区(根据 activeMode 从 result 里取对应字段)
*/ */
@@ -921,6 +930,12 @@ export class MeasurePanel implements IBimComponent {
} }
this.mainValueRowEl.style.display = ''; this.mainValueRowEl.style.display = '';
if (!this.activeMode) {
this.mainNumberEl.textContent = '--';
this.mainUnitEl.textContent = '';
this.xyzBoxEl.style.display = 'none';
return;
}
this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode)); this.mainValueLabelEl.textContent = t(this.getModeValueLabelI18nKey(this.activeMode));
const value = this.result ? (this.result as any)[MEASURE_TYPES[this.activeMode].callBackType] : undefined; const value = this.result ? (this.result as any)[MEASURE_TYPES[this.activeMode].callBackType] : undefined;

View File

@@ -226,9 +226,8 @@
.bim-tree-title { .bim-tree-title {
flex: 1; flex: 1;
white-space: nowrap; white-space: nowrap;
/* 移除省略号样式,许文本撑开 */ overflow: hidden;
/* overflow: hidden; */ text-overflow: ellipsis;
/* text-overflow: ellipsis; */
} }
/* 子节点容器 */ /* 子节点容器 */

View File

@@ -179,7 +179,7 @@ export class BimTree implements IBimComponent {
noData.className = 'bim-tree-search-item'; noData.className = 'bim-tree-search-item';
noData.style.cursor = 'default'; noData.style.cursor = 'default';
noData.style.color = '#999'; noData.style.color = '#999';
noData.textContent = 'No results'; noData.textContent = t('tree.noResults');
this.searchResults.appendChild(noData); this.searchResults.appendChild(noData);
} else { } else {
results.forEach(res => { results.forEach(res => {

View File

@@ -23,6 +23,17 @@ export class BimTreeNode {
private onNodeClick: (node: BimTreeNode) => void; private onNodeClick: (node: BimTreeNode) => void;
private renderActions?: (node: TreeNodeConfig) => HTMLElement | string; private renderActions?: (node: TreeNodeConfig) => HTMLElement | string;
private renderActionsContent(): void {
if (!this.renderActions) return;
const content = this.renderActions(this.config);
this.actionsEl.innerHTML = '';
if (typeof content === 'string') {
this.actionsEl.innerHTML = content;
} else if (content instanceof HTMLElement) {
this.actionsEl.appendChild(content);
}
}
constructor( constructor(
config: TreeNodeConfig, config: TreeNodeConfig,
options: TreeOptions, options: TreeOptions,
@@ -111,6 +122,7 @@ export class BimTreeNode {
this.actionsEl.addEventListener('click', (e) => { this.actionsEl.addEventListener('click', (e) => {
e.stopPropagation(); // 防止点击操作栏触发选中/展开 e.stopPropagation(); // 防止点击操作栏触发选中/展开
}); });
this.renderActionsContent();
this.contentEl.appendChild(this.actionsEl); this.contentEl.appendChild(this.actionsEl);
// 绑定整行点击 // 绑定整行点击
@@ -150,19 +162,8 @@ export class BimTreeNode {
public setSelected(selected: boolean) { public setSelected(selected: boolean) {
if (selected) { if (selected) {
this.contentEl.classList.add('is-selected'); this.contentEl.classList.add('is-selected');
// 渲染自定义操作栏
if (this.renderActions) {
const content = this.renderActions(this.config);
this.actionsEl.innerHTML = '';
if (typeof content === 'string') {
this.actionsEl.innerHTML = content;
} else if (content instanceof HTMLElement) {
this.actionsEl.appendChild(content);
}
}
} else { } else {
this.contentEl.classList.remove('is-selected'); this.contentEl.classList.remove('is-selected');
this.actionsEl.innerHTML = ''; // 清空内容
} }
} }

View File

@@ -1,196 +0,0 @@
.walk-control-panel {
display: flex;
align-items: center;
gap: 20px;
padding: 12px 20px;
background-color: var(--bim-bg-base, #152232);
border: 1px solid var(--bim-border-strong, #475569);
border-radius: 12px;
box-shadow: var(--bim-shadow-xl, 0 20px 40px rgba(0, 0, 0, 0.3));
user-select: none;
}
.walk-divider {
width: 1px;
height: 40px;
background: var(--bim-divider);
flex-shrink: 0;
}
/* 左侧按钮区 */
.walk-control-left {
display: flex;
gap: 8px;
}
.walk-icon-btn {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bim-bg-inset);
border: 1px solid var(--bim-border-default);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--bim-icon-default);
padding: 8px;
}
.walk-icon-btn:hover {
background: var(--bim-component-bg-hover);
border-color: var(--bim-border-strong);
}
.walk-icon-btn.active {
background: var(--bim-primary-subtle);
border-color: var(--bim-primary);
color: var(--bim-primary);
}
.walk-icon-btn.active svg {
fill: var(--bim-primary);
}
.walk-icon-btn svg {
width: 32px;
height: 32px;
}
/* 中间设置区 */
.walk-control-settings {
display: flex;
align-items: center;
gap: 16px;
flex: 1;
}
/* 速度控件 */
.walk-speed-control {
display: flex;
align-items: center;
gap: 12px;
}
.walk-speed-label {
color: var(--bim-text-primary);
font-size: 14px;
white-space: nowrap;
}
.walk-speed-group {
display: flex;
align-items: center;
gap: 8px;
background: var(--bim-bg-inset);
border: 1px solid var(--bim-border-subtle);
border-radius: 6px;
padding: 4px;
}
.walk-speed-btn {
width: 32px;
height: 32px;
background: var(--bim-bg-elevated);
border: 1px solid var(--bim-border-subtle);
border-radius: 4px;
color: var(--bim-text-primary);
font-size: 18px;
cursor: pointer;
transition: all 0.2s ease;
}
.walk-speed-btn:hover {
background: var(--bim-component-bg-hover);
border-color: var(--bim-border-strong);
}
.walk-speed-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.walk-speed-display {
min-width: 40px;
text-align: center;
color: var(--bim-text-primary);
font-size: 14px;
font-weight: bold;
}
/* 复选框 */
.walk-checkbox-wrapper {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.walk-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.walk-checkbox:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.walk-checkbox-label {
color: var(--bim-text-primary);
font-size: 14px;
white-space: nowrap;
}
.walk-checkbox-wrapper input:disabled + .walk-checkbox-label {
opacity: 0.5;
}
/* 下拉框 */
.walk-select-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.walk-select-label {
color: var(--bim-text-primary);
font-size: 14px;
white-space: nowrap;
}
.walk-select {
padding: 6px 12px;
background: var(--bim-bg-inset);
border: 1px solid var(--bim-border-default);
border-radius: 6px;
color: var(--bim-text-primary);
font-size: 14px;
cursor: pointer;
min-width: 120px;
}
.walk-select option {
background: var(--bim-bg-elevated);
color: var(--bim-text-primary);
}
.walk-exit-btn {
padding: 10px 24px;
background: var(--bim-primary);
border: none;
border-radius: 8px;
color: var(--bim-text-inverse);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.walk-exit-btn:hover {
background: var(--bim-primary-hover);
transform: scale(1.02);
}

View File

@@ -1,470 +0,0 @@
import './index.css';
import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { WalkControlPanelOptions, WalkControlState, WalkControlMode, CharacterModel, WalkMode } from './types';
import { getIcon } from '../../utils/icon-manager';
export class WalkControlPanel implements IBimComponent {
public element!: HTMLElement;
private options: WalkControlPanelOptions;
// 状态
private state: WalkControlState = {
mode: 'none',
isPlanViewActive: false,
speed: 1,
gravity: false,
collision: false,
characterModel: 'construction-worker',
walkMode: 'walk'
};
// DOM 引用 - 左侧按钮
private planViewBtn!: HTMLButtonElement;
private pathModeBtn!: HTMLButtonElement;
// private walkModeBtn!: HTMLButtonElement;
// DOM 引用 - 中间设置区
private settingsContainer!: HTMLElement;
private speedControl!: HTMLElement;
private speedDecreaseBtn!: HTMLButtonElement;
private speedIncreaseBtn!: HTMLButtonElement;
private speedDisplay!: HTMLElement;
private gravityCheckbox!: HTMLInputElement;
private gravityLabel!: HTMLElement;
private collisionCheckbox!: HTMLInputElement;
private collisionLabel!: HTMLElement;
private characterModelSelect!: HTMLSelectElement;
private characterModelLabel!: HTMLElement;
private walkModeSelect!: HTMLSelectElement;
private walkModeLabel!: HTMLElement;
// DOM 引用 - 退出按钮
private exitBtn!: HTMLButtonElement;
// 国际化订阅
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
constructor(options: WalkControlPanelOptions = {}) {
this.options = options;
this.state.speed = options.defaultSpeed ?? 1;
this.state.gravity = options.defaultGravity ?? false;
this.state.collision = options.defaultCollision ?? false;
this.state.characterModel = options.defaultCharacterModel ?? 'construction-worker';
this.state.walkMode = options.defaultWalkMode ?? 'walk';
}
public init(): void {
this.element = this.createPanel();
this.updateSettingsView();
// 订阅
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales();
this.setTheme(themeManager.getTheme());
}
// --- 公共方法 ---
public setPlanViewActive(active: boolean): void {
this.state.isPlanViewActive = active;
this.updateButtonStates();
}
public setPathModeActive(active: boolean): void {
// 只有当前是路径模式时,取消才设置为 none
// 避免在其他模式下被误设置为 none
if (!active && this.state.mode !== 'path') {
return;
}
const newMode: WalkControlMode = active ? 'path' : 'none';
this.setMode(newMode);
}
public getState(): WalkControlState {
return { ...this.state };
}
public setSpeed(speed: number): void {
this.state.speed = Math.max(1, Math.min(10, speed));
this.updateSpeedDisplay();
}
// --- 私有方法 ---
private createPanel(): HTMLElement {
const panel = document.createElement('div');
panel.className = 'walk-control-panel';
// 左侧按钮区
const leftButtons = this.createLeftButtons();
// 分割线1
const divider1 = document.createElement('div');
divider1.className = 'walk-divider';
// 中间设置区
this.settingsContainer = this.createSettingsContainer();
// 分割线2
const divider2 = document.createElement('div');
divider2.className = 'walk-divider';
// 右侧退出按钮
const exitBtn = this.createExitButton();
panel.appendChild(leftButtons);
panel.appendChild(divider1);
panel.appendChild(this.settingsContainer);
panel.appendChild(divider2);
panel.appendChild(exitBtn);
return panel;
}
private createLeftButtons(): HTMLElement {
const container = document.createElement('div');
container.className = 'walk-control-left';
this.planViewBtn = this.createIconButton('plan-view', () => {
this.state.isPlanViewActive = !this.state.isPlanViewActive;
this.updateButtonStates();
this.options.onPlanViewToggle?.(this.state.isPlanViewActive);
});
this.pathModeBtn = this.createIconButton('path', () => {
const newMode: WalkControlMode = this.state.mode === 'path' ? 'none' : 'path';
this.setMode(newMode);
this.options.onPathModeToggle?.(newMode === 'path');
});
// this.walkModeBtn = this.createIconButton('walk', () => {
// const newMode: WalkControlMode = this.state.mode === 'walk' ? 'none' : 'walk';
// this.setMode(newMode);
// this.options.onWalkModeToggle?.(newMode === 'walk');
// });
container.appendChild(this.planViewBtn);
return container;
}
private createSettingsContainer(): HTMLElement {
const container = document.createElement('div');
container.className = 'walk-control-settings';
// 移动速度控件
this.speedControl = this.createSpeedControl();
// 重力复选框
const gravityWrapper = document.createElement('label');
gravityWrapper.className = 'walk-checkbox-wrapper walk-checkbox-gravity';
this.gravityCheckbox = document.createElement('input');
this.gravityCheckbox.type = 'checkbox';
this.gravityCheckbox.className = 'walk-checkbox';
this.gravityCheckbox.checked = this.state.gravity;
this.gravityCheckbox.addEventListener('change', () => {
this.state.gravity = this.gravityCheckbox.checked;
this.options.onGravityToggle?.(this.state.gravity);
});
this.gravityLabel = document.createElement('span');
this.gravityLabel.className = 'walk-checkbox-label';
gravityWrapper.appendChild(this.gravityCheckbox);
gravityWrapper.appendChild(this.gravityLabel);
// 碰撞复选框
const collisionWrapper = document.createElement('label');
collisionWrapper.className = 'walk-checkbox-wrapper walk-checkbox-collision';
this.collisionCheckbox = document.createElement('input');
this.collisionCheckbox.type = 'checkbox';
this.collisionCheckbox.className = 'walk-checkbox';
this.collisionCheckbox.checked = this.state.collision;
this.collisionCheckbox.addEventListener('change', () => {
this.state.collision = this.collisionCheckbox.checked;
this.options.onCollisionToggle?.(this.state.collision);
});
this.collisionLabel = document.createElement('span');
this.collisionLabel.className = 'walk-checkbox-label';
collisionWrapper.appendChild(this.collisionCheckbox);
collisionWrapper.appendChild(this.collisionLabel);
// 角色模型选择
const characterWrapper = document.createElement('div');
characterWrapper.className = 'walk-select-wrapper walk-select-wrapper-character-model';
this.characterModelLabel = document.createElement('label');
this.characterModelLabel.className = 'walk-select-label';
this.characterModelSelect = document.createElement('select');
this.characterModelSelect.className = 'walk-select walk-select-character-model';
this.characterModelSelect.addEventListener('change', () => {
this.state.characterModel = this.characterModelSelect.value as CharacterModel;
this.options.onCharacterModelChange?.(this.state.characterModel);
});
characterWrapper.appendChild(this.characterModelLabel);
characterWrapper.appendChild(this.characterModelSelect);
// 行走模式选择
const walkModeWrapper = document.createElement('div');
walkModeWrapper.className = 'walk-select-wrapper walk-select-wrapper-walk-mode';
this.walkModeLabel = document.createElement('label');
this.walkModeLabel.className = 'walk-select-label';
this.walkModeSelect = document.createElement('select');
this.walkModeSelect.className = 'walk-select walk-select-walk-mode';
this.walkModeSelect.addEventListener('change', () => {
this.state.walkMode = this.walkModeSelect.value as WalkMode;
this.options.onWalkModeChange?.(this.state.walkMode);
});
walkModeWrapper.appendChild(this.walkModeLabel);
walkModeWrapper.appendChild(this.walkModeSelect);
// 添加所有控件
// 注意:顺序为 速度、角色模型、行走模式、重力、碰撞
// 这样在漫游模式下显示的顺序就是:角色模型、行走模式、重力、碰撞
container.appendChild(this.speedControl);
container.appendChild(characterWrapper);
container.appendChild(walkModeWrapper);
container.appendChild(gravityWrapper);
container.appendChild(collisionWrapper);
return container;
}
private createSpeedControl(): HTMLElement {
const container = document.createElement('div');
container.className = 'walk-speed-control';
const label = document.createElement('label');
label.className = 'walk-speed-label';
label.textContent = t('walkControl.speed');
const controlGroup = document.createElement('div');
controlGroup.className = 'walk-speed-group';
// 减速按钮
this.speedDecreaseBtn = document.createElement('button');
this.speedDecreaseBtn.className = 'walk-speed-btn';
this.speedDecreaseBtn.textContent = '-';
this.speedDecreaseBtn.addEventListener('click', () => {
if (this.state.speed > 1) {
this.state.speed--;
this.updateSpeedDisplay();
this.options.onSpeedChange?.(this.state.speed);
}
});
// 速度显示
this.speedDisplay = document.createElement('div');
this.speedDisplay.className = 'walk-speed-display';
this.speedDisplay.textContent = `${this.state.speed}X`;
// 加速按钮
this.speedIncreaseBtn = document.createElement('button');
this.speedIncreaseBtn.className = 'walk-speed-btn';
this.speedIncreaseBtn.textContent = '+';
this.speedIncreaseBtn.addEventListener('click', () => {
if (this.state.speed < 10) {
this.state.speed++;
this.updateSpeedDisplay();
this.options.onSpeedChange?.(this.state.speed);
}
});
controlGroup.appendChild(this.speedDecreaseBtn);
controlGroup.appendChild(this.speedDisplay);
controlGroup.appendChild(this.speedIncreaseBtn);
container.appendChild(label);
container.appendChild(controlGroup);
return container;
}
private createIconButton(type: string, onClick: () => void): HTMLButtonElement {
const btn = document.createElement('button');
btn.className = `walk-icon-btn walk-icon-btn-${type}`;
btn.innerHTML = this.getIconSVG(type);
btn.addEventListener('click', onClick);
return btn;
}
private createExitButton(): HTMLButtonElement {
const btn = document.createElement('button');
btn.className = 'walk-exit-btn';
btn.addEventListener('click', () => {
this.options.onExit?.();
});
this.exitBtn = btn;
return btn;
}
private setMode(mode: WalkControlMode): void {
const oldMode = this.state.mode;
// 如果从walk模式切换到其他模式触发walk关闭事件
if (oldMode === 'walk' && mode !== 'walk') {
this.options.onWalkModeToggle?.(false);
}
// 如果从path模式切换到其他模式触发path关闭事件
if (oldMode === 'path' && mode !== 'path') {
this.options.onPathModeToggle?.(false);
}
this.state.mode = mode;
// 路径模式:禁用重力和碰撞
if (mode === 'path') {
this.state.gravity = false;
this.state.collision = false;
this.gravityCheckbox.checked = false;
this.gravityCheckbox.disabled = true;
this.collisionCheckbox.checked = false;
this.collisionCheckbox.disabled = true;
} else {
this.gravityCheckbox.disabled = false;
this.collisionCheckbox.disabled = false;
}
this.updateButtonStates();
this.updateSettingsView();
this.updateSpeedButtonStates();
}
private updateButtonStates(): void {
// 平面图按钮
this.planViewBtn.classList.toggle('active', this.state.isPlanViewActive);
// 路径漫游按钮
this.pathModeBtn.classList.toggle('active', this.state.mode === 'path');
// 漫游按钮
// this.walkModeBtn.classList.toggle('active', this.state.mode === 'walk');
}
private updateSettingsView(): void {
// 根据模式显示/隐藏不同的控件
const speedWrapper = this.speedControl;
const gravityWrapper = this.gravityCheckbox.parentElement!;
const collisionWrapper = this.collisionCheckbox.parentElement!;
const characterWrapper = this.characterModelSelect.parentElement!;
const walkModeWrapper = this.walkModeSelect.parentElement!;
if (this.state.mode === 'walk') {
// 漫游模式:隐藏速度,显示模型、行走模式、重力、碰撞
speedWrapper.style.display = 'none';
gravityWrapper.style.display = 'flex';
collisionWrapper.style.display = 'flex';
characterWrapper.style.display = 'flex';
walkModeWrapper.style.display = 'flex';
} else {
// 默认或路径模式:显示速度、重力、碰撞,隐藏模型和行走模式
speedWrapper.style.display = 'flex';
gravityWrapper.style.display = 'flex';
collisionWrapper.style.display = 'flex';
characterWrapper.style.display = 'none';
walkModeWrapper.style.display = 'none';
}
}
private updateSpeedDisplay(): void {
this.speedDisplay.textContent = `${this.state.speed}X`;
this.updateSpeedButtonStates();
}
private updateSpeedButtonStates(): void {
this.speedDecreaseBtn.disabled = this.state.speed <= 1;
this.speedIncreaseBtn.disabled = this.state.speed >= 10;
}
private getIconSVG(type: string): string {
const icons: Record<string, string> = {
'plan-view': getIcon('地图'),
'path': getIcon('路径漫游'),
'walk': getIcon('第一人称漫游')
};
return icons[type] || '';
}
public setLocales(): void {
// 更新速度标签
const speedLabel = this.speedControl.querySelector('.walk-speed-label');
if (speedLabel) {
speedLabel.textContent = t('walkControl.speed');
}
// 更新复选框标签
this.gravityLabel.textContent = t('walkControl.gravity');
this.collisionLabel.textContent = t('walkControl.collision');
// 更新角色模型下拉框
this.characterModelLabel.textContent = t('walkControl.characterModel.label');
this.characterModelSelect.innerHTML = '';
const constructionWorkerOption = document.createElement('option');
constructionWorkerOption.value = 'construction-worker';
constructionWorkerOption.textContent = t('walkControl.characterModel.constructionWorker');
constructionWorkerOption.selected = this.state.characterModel === 'construction-worker';
this.characterModelSelect.appendChild(constructionWorkerOption);
const officeMaleOption = document.createElement('option');
officeMaleOption.value = 'office-male';
officeMaleOption.textContent = t('walkControl.characterModel.officeMale');
officeMaleOption.selected = this.state.characterModel === 'office-male';
this.characterModelSelect.appendChild(officeMaleOption);
// 更新行走模式下拉框
this.walkModeLabel.textContent = t('walkControl.walkMode.label');
this.walkModeSelect.innerHTML = '';
const walkOption = document.createElement('option');
walkOption.value = 'walk';
walkOption.textContent = t('walkControl.walkMode.walk');
walkOption.selected = this.state.walkMode === 'walk';
this.walkModeSelect.appendChild(walkOption);
const runOption = document.createElement('option');
runOption.value = 'run';
runOption.textContent = t('walkControl.walkMode.run');
runOption.selected = this.state.walkMode === 'run';
this.walkModeSelect.appendChild(runOption);
// 更新退出按钮
this.exitBtn.textContent = t('walkControl.exit');
}
public setTheme(theme: ThemeConfig): void {
if (!this.element) return;
const style = this.element.style;
style.setProperty('--bim-bg-base', theme.bgBase ?? '#152232');
style.setProperty('--bim-bg-elevated', theme.bgElevated ?? '#1f2d3e');
style.setProperty('--bim-bg-inset', theme.bgInset ?? '#152232');
style.setProperty('--bim-border-default', theme.borderDefault ?? '#334155');
style.setProperty('--bim-border-strong', theme.borderStrong ?? '#475569');
style.setProperty('--bim-border-subtle', theme.borderSubtle ?? '#1e293b');
style.setProperty('--bim-divider', theme.divider ?? '#334155');
style.setProperty('--bim-shadow-xl', theme.shadowXl ?? '0 20px 40px rgba(0, 0, 0, 0.3)');
style.setProperty('--bim-primary', theme.primary ?? '#3b82f6');
style.setProperty('--bim-primary-hover', theme.primaryHover ?? '#60a5fa');
style.setProperty('--bim-primary-subtle', theme.primarySubtle ?? 'rgba(59, 130, 246, 0.15)');
style.setProperty('--bim-text-primary', theme.textPrimary ?? '#ffffff');
style.setProperty('--bim-text-inverse', theme.textInverse ?? '#152232');
style.setProperty('--bim-icon-default', theme.iconDefault ?? '#ffffff');
style.setProperty('--bim-component-bg-hover', theme.componentBgHover ?? 'rgba(248, 250, 252, 0.06)');
style.setProperty('--bim-component-bg-active', theme.componentBgActive ?? 'rgba(248, 250, 252, 0.1)');
}
public destroy(): void {
this.unsubscribeLocale?.();
this.unsubscribeTheme?.();
if (this.element && this.element.parentElement) {
this.element.parentElement.removeChild(this.element);
}
}
}

View File

@@ -1,69 +0,0 @@
/**
* 漫游控制模式
*/
export type WalkControlMode = 'none' | 'path' | 'walk';
/**
* 角色模型类型
*/
export type CharacterModel = 'office-male' | 'construction-worker';
/**
* 行走模式类型
*/
export type WalkMode = 'walk' | 'run';
/**
* 漫游控制面板配置选项
*/
export interface WalkControlPanelOptions {
/** 平面图切换回调 */
onPlanViewToggle?: (isActive: boolean) => void;
/** 路径漫游模式切换回调 */
onPathModeToggle?: (isActive: boolean) => void;
/** 漫游模式切换回调 */
onWalkModeToggle?: (isActive: boolean) => void;
/** 速度变化回调 */
onSpeedChange?: (speed: number) => void;
/** 重力切换回调 */
onGravityToggle?: (enabled: boolean) => void;
/** 碰撞切换回调 */
onCollisionToggle?: (enabled: boolean) => void;
/** 角色模型变化回调 */
onCharacterModelChange?: (model: CharacterModel) => void;
/** 行走模式变化回调 */
onWalkModeChange?: (mode: WalkMode) => void;
/** 退出回调 */
onExit?: () => void;
/** 默认速度 (0-100) */
defaultSpeed?: number;
/** 默认重力状态 */
defaultGravity?: boolean;
/** 默认碰撞状态 */
defaultCollision?: boolean;
/** 默认角色模型 */
defaultCharacterModel?: CharacterModel;
/** 默认行走模式 */
defaultWalkMode?: WalkMode;
}
/**
* 漫游控制状态
*/
export interface WalkControlState {
/** 当前模式 */
mode: WalkControlMode;
/** 平面图是否激活 */
isPlanViewActive: boolean;
/** 移动速度 (0-100) */
speed: number;
/** 重力是否启用 */
gravity: boolean;
/** 碰撞是否启用 */
collision: boolean;
/** 当前角色模型 */
characterModel: CharacterModel;
/** 当前行走模式 */
walkMode: WalkMode;
}

View File

@@ -1,4 +1,6 @@
.walk-control-panel.walk-dock-panel { .walk-dock-panel {
display: flex;
align-items: center;
gap: 10px; gap: 10px;
padding: 0; padding: 0;
background: transparent; background: transparent;
@@ -6,64 +8,88 @@
border-radius: 8px; border-radius: 8px;
box-shadow: none; box-shadow: none;
color: var(--bim-text-secondary, #64748b); color: var(--bim-text-secondary, #64748b);
user-select: none;
} }
.walk-control-panel.walk-dock-panel .walk-divider { .walk-dock-panel .walk-divider {
height: 32px; height: 32px;
width: 1px;
background: var(--bim-divider);
flex-shrink: 0;
} }
.walk-control-panel.walk-dock-panel .walk-control-left, .walk-dock-left {
.walk-control-panel.walk-dock-panel .walk-control-settings { display: flex;
gap: 10px; gap: 10px;
} }
.walk-control-panel.walk-dock-panel .walk-icon-btn { .walk-dock-settings {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.walk-icon-btn {
width: 32px; width: 32px;
height: 32px; height: 32px;
display: flex;
align-items: center;
justify-content: center;
padding: 0; padding: 0;
border-radius: 8px; border-radius: 8px;
border: 1px solid rgba(148, 163, 184, 0.28); border: 1px solid rgba(148, 163, 184, 0.28);
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%); background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
color: color-mix(in srgb, var(--bim-text-secondary, #64748b) 94%, #475569 6%); color: color-mix(in srgb, var(--bim-text-secondary, #64748b) 94%, #475569 6%);
cursor: pointer;
transition: all 0.2s ease;
} }
.walk-control-panel.walk-dock-panel .walk-icon-btn:hover { .walk-icon-btn:hover {
border-color: rgba(148, 163, 184, 0.5); border-color: rgba(148, 163, 184, 0.5);
background: color-mix(in srgb, var(--bim-component-bg-hover, #dce5f2) 64%, #ffffff 36%); background: color-mix(in srgb, var(--bim-component-bg-hover, #dce5f2) 64%, #ffffff 36%);
} }
.walk-control-panel.walk-dock-panel .walk-icon-btn.active { .walk-icon-btn.active {
border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%); border-color: color-mix(in srgb, var(--bim-primary, #4f88ff) 70%, #9db9ff 30%);
background: color-mix(in srgb, var(--bim-primary-subtle, rgba(96, 140, 255, 0.18)) 72%, #ffffff 28%); background: color-mix(in srgb, var(--bim-primary-subtle, rgba(96, 140, 255, 0.18)) 72%, #ffffff 28%);
color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%); color: color-mix(in srgb, var(--bim-primary, #4f88ff) 78%, #6f9dff 22%);
} }
.walk-control-panel.walk-dock-panel .walk-icon-btn svg { .walk-icon-btn svg {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
.walk-control-panel.walk-dock-panel .walk-icon-btn.active svg { .walk-icon-btn.active svg {
fill: currentColor; fill: currentColor;
} }
.walk-control-panel.walk-dock-panel .walk-speed-control { .walk-speed-control {
display: flex;
align-items: center;
gap: 8px; gap: 8px;
} }
.walk-control-panel.walk-dock-panel .walk-speed-label, .walk-speed-label,
.walk-control-panel.walk-dock-panel .walk-checkbox-label, .walk-checkbox-label,
.walk-control-panel.walk-dock-panel .walk-select-label { .walk-select-label {
font-size: 12px; font-size: 12px;
color: var(--bim-text-primary);
white-space: nowrap;
} }
.walk-control-panel.walk-dock-panel .walk-speed-group { .walk-speed-group {
display: flex;
align-items: center;
gap: 8px;
padding: 2px; padding: 2px;
border: 1px solid rgba(148, 163, 184, 0.28); border: 1px solid rgba(148, 163, 184, 0.28);
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%); background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
border-radius: 6px;
} }
.walk-control-panel.walk-dock-panel .walk-speed-btn { .walk-speed-btn {
width: 24px; width: 24px;
height: 24px; height: 24px;
font-size: 14px; font-size: 14px;
@@ -75,21 +101,55 @@
border: 1px solid rgba(148, 163, 184, 0.28); border: 1px solid rgba(148, 163, 184, 0.28);
background: color-mix(in srgb, var(--bim-bg-elevated, #f8fafc) 88%, #ffffff 12%); background: color-mix(in srgb, var(--bim-bg-elevated, #f8fafc) 88%, #ffffff 12%);
color: var(--bim-text-primary, #0f172a); color: var(--bim-text-primary, #0f172a);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
} }
.walk-control-panel.walk-dock-panel .walk-speed-display { .walk-speed-btn:hover {
border-color: rgba(148, 163, 184, 0.5);
}
.walk-speed-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.walk-speed-display {
min-width: 30px; min-width: 30px;
font-size: 12px; font-size: 12px;
color: var(--bim-text-primary, #0f172a); color: var(--bim-text-primary, #0f172a);
text-align: center;
font-weight: bold;
} }
.walk-control-panel.walk-dock-panel .walk-checkbox { .walk-checkbox-wrapper {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.walk-checkbox {
width: 14px; width: 14px;
height: 14px; height: 14px;
accent-color: var(--bim-primary, #3b82f6); accent-color: var(--bim-primary, #3b82f6);
cursor: pointer;
} }
.walk-control-panel.walk-dock-panel .walk-select { .walk-checkbox:disabled,
.walk-checkbox-wrapper input:disabled + .walk-checkbox-label {
opacity: 0.5;
cursor: not-allowed;
}
.walk-select-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.walk-select {
min-width: 90px; min-width: 90px;
height: 24px; height: 24px;
padding: 2px 8px; padding: 2px 8px;
@@ -97,11 +157,30 @@
border: 1px solid rgba(148, 163, 184, 0.28); border: 1px solid rgba(148, 163, 184, 0.28);
background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%); background: color-mix(in srgb, var(--bim-bg-inset, #edf1f6) 92%, #ffffff 8%);
color: var(--bim-text-primary, #0f172a); color: var(--bim-text-primary, #0f172a);
border-radius: 6px;
cursor: pointer;
} }
.walk-control-panel.walk-dock-panel .walk-exit-btn { .walk-select option {
background: var(--bim-bg-elevated);
color: var(--bim-text-primary);
}
.walk-exit-btn {
height: 32px; height: 32px;
padding: 0 12px; padding: 0 12px;
border-radius: 8px; border-radius: 8px;
font-size: 12px; font-size: 12px;
background: var(--bim-primary);
border: none;
color: var(--bim-text-inverse);
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.walk-exit-btn:hover {
background: var(--bim-primary-hover);
transform: scale(1.02);
} }

View File

@@ -1,43 +1,423 @@
import './index.css'; import './index.css';
import { IBimComponent } from '../../types/component';
import { WalkControlPanel } from '../walk-control-panel';
import type { WalkControlPanelOptions, WalkControlState } from '../walk-control-panel/types';
import type { ThemeConfig } from '../../themes/types'; import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { WalkDockPanelOptions, WalkDockMode, CharacterModel, WalkMode } from './types';
import { getIcon } from '../../utils/icon-manager';
export class WalkDockPanel implements IBimComponent { export class WalkDockPanel implements IBimComponent {
public readonly element: HTMLElement; public element!: HTMLElement;
private readonly panel: WalkControlPanel; private options: WalkDockPanelOptions;
constructor(options: WalkControlPanelOptions = {}) { private state = {
this.panel = new WalkControlPanel(options); mode: 'none' as WalkDockMode,
this.panel.init(); isPlanViewActive: false,
this.element = this.panel.element; speed: 1,
this.element.classList.add('walk-dock-panel'); gravity: false,
collision: false,
characterModel: 'construction-worker' as CharacterModel,
walkMode: 'walk' as WalkMode
};
private planViewBtn!: HTMLButtonElement;
private pathModeBtn!: HTMLButtonElement;
private settingsContainer!: HTMLElement;
private speedControl!: HTMLElement;
private speedDecreaseBtn!: HTMLButtonElement;
private speedIncreaseBtn!: HTMLButtonElement;
private speedDisplay!: HTMLElement;
private gravityCheckbox!: HTMLInputElement;
private gravityLabel!: HTMLElement;
private collisionCheckbox!: HTMLInputElement;
private collisionLabel!: HTMLElement;
private characterModelSelect!: HTMLSelectElement;
private characterModelLabel!: HTMLElement;
private walkModeSelect!: HTMLSelectElement;
private walkModeLabel!: HTMLElement;
private exitBtn!: HTMLButtonElement;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
constructor(options: WalkDockPanelOptions = {}) {
this.options = options;
} }
public init(): void {} public init(): void {
this.element = this.createPanel();
this.updateSettingsView();
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
this.setLocales();
this.setTheme(themeManager.getTheme());
}
public setPlanViewActive(active: boolean): void { public setPlanViewActive(active: boolean): void {
this.panel.setPlanViewActive(active); this.state.isPlanViewActive = active;
this.updateButtonStates();
} }
public setLocales(): void { public setPathModeActive(active: boolean): void {
this.panel.setLocales(); if (!active && this.state.mode !== 'path') {
return;
}
const newMode: WalkDockMode = active ? 'path' : 'none';
this.setMode(newMode);
} }
public setTheme(theme: ThemeConfig): void { public getState() {
this.panel.setTheme(theme); return { ...this.state };
}
public getState(): WalkControlState {
return this.panel.getState();
} }
public setSpeed(speed: number): void { public setSpeed(speed: number): void {
this.panel.setSpeed(speed); this.state.speed = Math.max(1, Math.min(10, speed));
this.updateSpeedDisplay();
}
public setGravity(enabled: boolean): void {
this.state.gravity = enabled;
if (this.gravityCheckbox) {
this.gravityCheckbox.checked = enabled;
}
}
public setCollision(enabled: boolean): void {
this.state.collision = enabled;
if (this.collisionCheckbox) {
this.collisionCheckbox.checked = enabled;
}
}
private createPanel(): HTMLElement {
const panel = document.createElement('div');
panel.className = 'walk-dock-panel';
const leftButtons = this.createLeftButtons();
const divider1 = document.createElement('div');
divider1.className = 'walk-divider';
this.settingsContainer = this.createSettingsContainer();
const divider2 = document.createElement('div');
divider2.className = 'walk-divider';
const exitBtn = this.createExitButton();
panel.appendChild(leftButtons);
panel.appendChild(divider1);
panel.appendChild(this.settingsContainer);
panel.appendChild(divider2);
panel.appendChild(exitBtn);
return panel;
}
private createLeftButtons(): HTMLElement {
const container = document.createElement('div');
container.className = 'walk-dock-left';
this.planViewBtn = this.createIconButton('plan-view', () => {
this.state.isPlanViewActive = !this.state.isPlanViewActive;
this.updateButtonStates();
this.options.onPlanViewToggle?.(this.state.isPlanViewActive);
});
this.pathModeBtn = this.createIconButton('path', () => {
const newMode: WalkDockMode = this.state.mode === 'path' ? 'none' : 'path';
this.setMode(newMode);
this.options.onPathModeToggle?.(newMode === 'path');
});
container.appendChild(this.planViewBtn);
return container;
}
private createSettingsContainer(): HTMLElement {
const container = document.createElement('div');
container.className = 'walk-dock-settings';
this.speedControl = this.createSpeedControl();
const gravityWrapper = document.createElement('label');
gravityWrapper.className = 'walk-checkbox-wrapper walk-checkbox-gravity';
this.gravityCheckbox = document.createElement('input');
this.gravityCheckbox.type = 'checkbox';
this.gravityCheckbox.className = 'walk-checkbox';
this.gravityCheckbox.checked = this.state.gravity;
this.gravityCheckbox.addEventListener('change', () => {
this.state.gravity = this.gravityCheckbox.checked;
this.options.onGravityToggle?.(this.state.gravity);
});
this.gravityLabel = document.createElement('span');
this.gravityLabel.className = 'walk-checkbox-label';
gravityWrapper.appendChild(this.gravityCheckbox);
gravityWrapper.appendChild(this.gravityLabel);
const collisionWrapper = document.createElement('label');
collisionWrapper.className = 'walk-checkbox-wrapper walk-checkbox-collision';
this.collisionCheckbox = document.createElement('input');
this.collisionCheckbox.type = 'checkbox';
this.collisionCheckbox.className = 'walk-checkbox';
this.collisionCheckbox.checked = this.state.collision;
this.collisionCheckbox.addEventListener('change', () => {
this.state.collision = this.collisionCheckbox.checked;
this.options.onCollisionToggle?.(this.state.collision);
});
this.collisionLabel = document.createElement('span');
this.collisionLabel.className = 'walk-checkbox-label';
collisionWrapper.appendChild(this.collisionCheckbox);
collisionWrapper.appendChild(this.collisionLabel);
const characterWrapper = document.createElement('div');
characterWrapper.className = 'walk-select-wrapper walk-select-wrapper-character-model';
this.characterModelLabel = document.createElement('label');
this.characterModelLabel.className = 'walk-select-label';
this.characterModelSelect = document.createElement('select');
this.characterModelSelect.className = 'walk-select walk-select-character-model';
this.characterModelSelect.addEventListener('change', () => {
this.state.characterModel = this.characterModelSelect.value as CharacterModel;
this.options.onCharacterModelChange?.(this.state.characterModel);
});
characterWrapper.appendChild(this.characterModelLabel);
characterWrapper.appendChild(this.characterModelSelect);
const walkModeWrapper = document.createElement('div');
walkModeWrapper.className = 'walk-select-wrapper walk-select-wrapper-walk-mode';
this.walkModeLabel = document.createElement('label');
this.walkModeLabel.className = 'walk-select-label';
this.walkModeSelect = document.createElement('select');
this.walkModeSelect.className = 'walk-select walk-select-walk-mode';
this.walkModeSelect.addEventListener('change', () => {
this.state.walkMode = this.walkModeSelect.value as WalkMode;
this.options.onWalkModeChange?.(this.state.walkMode);
});
walkModeWrapper.appendChild(this.walkModeLabel);
walkModeWrapper.appendChild(this.walkModeSelect);
container.appendChild(this.speedControl);
container.appendChild(characterWrapper);
container.appendChild(walkModeWrapper);
container.appendChild(gravityWrapper);
container.appendChild(collisionWrapper);
return container;
}
private createSpeedControl(): HTMLElement {
const container = document.createElement('div');
container.className = 'walk-speed-control';
const label = document.createElement('label');
label.className = 'walk-speed-label';
label.textContent = t('walkControl.speed');
const controlGroup = document.createElement('div');
controlGroup.className = 'walk-speed-group';
this.speedDecreaseBtn = document.createElement('button');
this.speedDecreaseBtn.className = 'walk-speed-btn';
this.speedDecreaseBtn.textContent = '-';
this.speedDecreaseBtn.addEventListener('click', () => {
if (this.state.speed > 1) {
this.state.speed--;
this.updateSpeedDisplay();
this.options.onSpeedChange?.(this.state.speed);
}
});
this.speedDisplay = document.createElement('div');
this.speedDisplay.className = 'walk-speed-display';
this.speedDisplay.textContent = `${this.state.speed}X`;
this.speedIncreaseBtn = document.createElement('button');
this.speedIncreaseBtn.className = 'walk-speed-btn';
this.speedIncreaseBtn.textContent = '+';
this.speedIncreaseBtn.addEventListener('click', () => {
if (this.state.speed < 10) {
this.state.speed++;
this.updateSpeedDisplay();
this.options.onSpeedChange?.(this.state.speed);
}
});
controlGroup.appendChild(this.speedDecreaseBtn);
controlGroup.appendChild(this.speedDisplay);
controlGroup.appendChild(this.speedIncreaseBtn);
container.appendChild(label);
container.appendChild(controlGroup);
return container;
}
private createIconButton(type: string, onClick: () => void): HTMLButtonElement {
const btn = document.createElement('button');
btn.className = `walk-icon-btn walk-icon-btn-${type}`;
btn.innerHTML = this.getIconSVG(type);
btn.addEventListener('click', onClick);
return btn;
}
private createExitButton(): HTMLButtonElement {
const btn = document.createElement('button');
btn.className = 'walk-exit-btn';
btn.addEventListener('click', () => {
this.options.onExit?.();
});
this.exitBtn = btn;
return btn;
}
private setMode(mode: WalkDockMode): void {
const oldMode = this.state.mode;
if (oldMode === 'walk' && mode !== 'walk') {
this.options.onWalkModeToggle?.(false);
}
if (oldMode === 'path' && mode !== 'path') {
this.options.onPathModeToggle?.(false);
}
this.state.mode = mode;
if (mode === 'path') {
this.state.gravity = false;
this.state.collision = false;
this.gravityCheckbox.checked = false;
this.gravityCheckbox.disabled = true;
this.collisionCheckbox.checked = false;
this.collisionCheckbox.disabled = true;
} else {
this.gravityCheckbox.disabled = false;
this.collisionCheckbox.disabled = false;
}
this.updateButtonStates();
this.updateSettingsView();
this.updateSpeedButtonStates();
}
private updateButtonStates(): void {
this.planViewBtn.classList.toggle('active', this.state.isPlanViewActive);
this.pathModeBtn.classList.toggle('active', this.state.mode === 'path');
}
private updateSettingsView(): void {
const speedWrapper = this.speedControl;
const gravityWrapper = this.gravityCheckbox.parentElement!;
const collisionWrapper = this.collisionCheckbox.parentElement!;
const characterWrapper = this.characterModelSelect.parentElement!;
const walkModeWrapper = this.walkModeSelect.parentElement!;
if (this.state.mode === 'walk') {
speedWrapper.style.display = 'none';
gravityWrapper.style.display = 'flex';
collisionWrapper.style.display = 'flex';
characterWrapper.style.display = 'flex';
walkModeWrapper.style.display = 'flex';
} else {
speedWrapper.style.display = 'flex';
gravityWrapper.style.display = 'flex';
collisionWrapper.style.display = 'flex';
characterWrapper.style.display = 'none';
walkModeWrapper.style.display = 'none';
}
}
private updateSpeedDisplay(): void {
this.speedDisplay.textContent = `${this.state.speed}X`;
this.updateSpeedButtonStates();
}
private updateSpeedButtonStates(): void {
this.speedDecreaseBtn.disabled = this.state.speed <= 1;
this.speedIncreaseBtn.disabled = this.state.speed >= 10;
}
private getIconSVG(type: string): string {
const icons: Record<string, string> = {
'plan-view': getIcon('地图'),
'path': getIcon('路径漫游'),
'walk': getIcon('第一人称漫游')
};
return icons[type] || '';
}
public setLocales(): void {
const speedLabel = this.speedControl.querySelector('.walk-speed-label');
if (speedLabel) {
speedLabel.textContent = t('walkControl.speed');
}
this.gravityLabel.textContent = t('walkControl.gravity');
this.collisionLabel.textContent = t('walkControl.collision');
this.characterModelLabel.textContent = t('walkControl.characterModel.label');
this.characterModelSelect.innerHTML = '';
const constructionWorkerOption = document.createElement('option');
constructionWorkerOption.value = 'construction-worker';
constructionWorkerOption.textContent = t('walkControl.characterModel.constructionWorker');
constructionWorkerOption.selected = this.state.characterModel === 'construction-worker';
this.characterModelSelect.appendChild(constructionWorkerOption);
const officeMaleOption = document.createElement('option');
officeMaleOption.value = 'office-male';
officeMaleOption.textContent = t('walkControl.characterModel.officeMale');
officeMaleOption.selected = this.state.characterModel === 'office-male';
this.characterModelSelect.appendChild(officeMaleOption);
this.walkModeLabel.textContent = t('walkControl.walkMode.label');
this.walkModeSelect.innerHTML = '';
const walkOption = document.createElement('option');
walkOption.value = 'walk';
walkOption.textContent = t('walkControl.walkMode.walk');
walkOption.selected = this.state.walkMode === 'walk';
this.walkModeSelect.appendChild(walkOption);
const runOption = document.createElement('option');
runOption.value = 'run';
runOption.textContent = t('walkControl.walkMode.run');
runOption.selected = this.state.walkMode === 'run';
this.walkModeSelect.appendChild(runOption);
this.exitBtn.textContent = t('walkControl.exit');
}
public setTheme(theme: ThemeConfig): void {
if (!this.element) return;
const style = this.element.style;
style.setProperty('--bim-bg-base', theme.bgBase ?? '#152232');
style.setProperty('--bim-bg-elevated', theme.bgElevated ?? '#1f2d3e');
style.setProperty('--bim-bg-inset', theme.bgInset ?? '#152232');
style.setProperty('--bim-border-default', theme.borderDefault ?? '#334155');
style.setProperty('--bim-border-strong', theme.borderStrong ?? '#475569');
style.setProperty('--bim-border-subtle', theme.borderSubtle ?? '#1e293b');
style.setProperty('--bim-divider', theme.divider ?? '#334155');
style.setProperty('--bim-shadow-xl', theme.shadowXl ?? '0 20px 40px rgba(0, 0, 0, 0.3)');
style.setProperty('--bim-primary', theme.primary ?? '#3b82f6');
style.setProperty('--bim-primary-hover', theme.primaryHover ?? '#60a5fa');
style.setProperty('--bim-primary-subtle', theme.primarySubtle ?? 'rgba(59, 130, 246, 0.15)');
style.setProperty('--bim-text-primary', theme.textPrimary ?? '#ffffff');
style.setProperty('--bim-text-inverse', theme.textInverse ?? '#152232');
style.setProperty('--bim-icon-default', theme.iconDefault ?? '#ffffff');
style.setProperty('--bim-component-bg-hover', theme.componentBgHover ?? 'rgba(248, 250, 252, 0.06)');
style.setProperty('--bim-component-bg-active', theme.componentBgActive ?? 'rgba(248, 250, 252, 0.1)');
} }
public destroy(): void { public destroy(): void {
this.panel.destroy(); this.unsubscribeLocale?.();
this.unsubscribeTheme?.();
if (this.element && this.element.parentElement) {
this.element.parentElement.removeChild(this.element);
}
} }
} }

View File

@@ -0,0 +1,38 @@
/**
* 漫游 Dock 模式
*/
export type WalkDockMode = 'none' | 'path' | 'walk';
/**
* 角色模型类型
*/
export type CharacterModel = 'office-male' | 'construction-worker';
/**
* 行走模式类型
*/
export type WalkMode = 'walk' | 'run';
/**
* 漫游 Dock 面板配置选项
*/
export interface WalkDockPanelOptions {
/** 平面图切换回调 */
onPlanViewToggle?: (isActive: boolean) => void;
/** 路径漫游模式切换回调 */
onPathModeToggle?: (isActive: boolean) => void;
/** 漫游模式切换回调 */
onWalkModeToggle?: (isActive: boolean) => void;
/** 速度变化回调 */
onSpeedChange?: (speed: number) => void;
/** 重力切换回调 */
onGravityToggle?: (enabled: boolean) => void;
/** 碰撞切换回调 */
onCollisionToggle?: (enabled: boolean) => void;
/** 角色模型变化回调 */
onCharacterModelChange?: (model: CharacterModel) => void;
/** 行走模式变化回调 */
onWalkModeChange?: (mode: WalkMode) => void;
/** 退出回调 */
onExit?: () => void;
}

View File

@@ -48,6 +48,21 @@ export abstract class BaseDialogManager extends BaseManager {
return { draggable: true, resizable: false }; return { draggable: true, resizable: false };
} }
/**
* 自定义标题栏内容(可选)。
* 返回 HTMLElement 时将完全替换默认标题栏title + close 按钮)。
* 适用于需要在标题栏插入 tab、图标、图片等自定义布局的场景。
* 与 dialogTitle 互斥,优先使用此值。
*/
protected get dialogHeader(): HTMLElement | undefined {
return undefined;
}
/** 是否隐藏关闭按钮(默认 false */
protected get dialogHideCloseButton(): boolean {
return false;
}
/** 创建对话框内容(子类必须实现) */ /** 创建对话框内容(子类必须实现) */
protected abstract createContent(): HTMLElement; protected abstract createContent(): HTMLElement;
@@ -102,6 +117,8 @@ export abstract class BaseDialogManager extends BaseManager {
position, position,
draggable: options.draggable, draggable: options.draggable,
resizable: options.resizable, resizable: options.resizable,
header: this.dialogHeader,
hideCloseButton: this.dialogHideCloseButton,
onClose: () => { onClose: () => {
this.onDialogClose(); this.onDialogClose();
this.destroyDialog(); this.destroyDialog();

View File

@@ -13,7 +13,6 @@ import type { RightKeyManager } from '../managers/right-key-manager';
import type { ConstructTreeManagerBtn } from '../managers/construct-tree-manager-btn'; import type { ConstructTreeManagerBtn } from '../managers/construct-tree-manager-btn';
import type { MeasureDialogManager } from '../managers/measure-dialog-manager'; import type { MeasureDialogManager } from '../managers/measure-dialog-manager';
import type { WalkControlManager } from '../managers/walk-control-manager';
import type { SectionPlaneDialogManager } from '../managers/section-plane-dialog-manager'; import type { SectionPlaneDialogManager } from '../managers/section-plane-dialog-manager';
import type { SectionAxisDialogManager } from '../managers/section-axis-dialog-manager'; import type { SectionAxisDialogManager } from '../managers/section-axis-dialog-manager';
import type { SectionBoxDialogManager } from '../managers/section-box-dialog-manager'; import type { SectionBoxDialogManager } from '../managers/section-box-dialog-manager';
@@ -28,6 +27,7 @@ import type { BottomDockManager } from '../managers/bottom-dock-manager';
import type { MeasureDockManager } from '../managers/measure-dock-manager'; import type { MeasureDockManager } from '../managers/measure-dock-manager';
import type { SectionDockManager } from '../managers/section-dock-manager'; import type { SectionDockManager } from '../managers/section-dock-manager';
import type { WalkDockManager } from '../managers/walk-dock-manager'; import type { WalkDockManager } from '../managers/walk-dock-manager';
import type { ComponentTreeDrawerManager } from '../managers/component-tree-drawer-manager';
/** /**
* Manager 注册表 - 实例模式 * Manager 注册表 - 实例模式
@@ -57,8 +57,6 @@ export class ManagerRegistry {
/** 测量对话框管理器 */ /** 测量对话框管理器 */
public measure: MeasureDialogManager | null = null; public measure: MeasureDialogManager | null = null;
/** 漫游控制管理器 */
public walkControl: WalkControlManager | null = null;
/** 拾取面剖切对话框管理器 */ /** 拾取面剖切对话框管理器 */
public sectionPlane: SectionPlaneDialogManager | null = null; public sectionPlane: SectionPlaneDialogManager | null = null;
/** 轴向剖切对话框管理器 */ /** 轴向剖切对话框管理器 */
@@ -83,6 +81,7 @@ export class ManagerRegistry {
public measureDock: MeasureDockManager | null = null; public measureDock: MeasureDockManager | null = null;
public sectionDock: SectionDockManager | null = null; public sectionDock: SectionDockManager | null = null;
public walkDock: WalkDockManager | null = null; public walkDock: WalkDockManager | null = null;
public componentTreeDrawer: ComponentTreeDrawerManager | null = null;
constructor() {} constructor() {}
@@ -98,7 +97,6 @@ export class ManagerRegistry {
this.rightKey = null; this.rightKey = null;
this.constructTree = null; this.constructTree = null;
this.measure = null; this.measure = null;
this.walkControl = null;
this.sectionPlane = null; this.sectionPlane = null;
this.sectionAxis = null; this.sectionAxis = null;
this.sectionBox = null; this.sectionBox = null;
@@ -113,6 +111,7 @@ export class ManagerRegistry {
this.measureDock = null; this.measureDock = null;
this.sectionDock = null; this.sectionDock = null;
this.walkDock = null; this.walkDock = null;
this.componentTreeDrawer = null;
} }
/** /**

View File

@@ -13,7 +13,6 @@ import { MeasureDialogManager } from './managers/measure-dialog-manager';
import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manager'; import { SectionPlaneDialogManager } from './managers/section-plane-dialog-manager';
import { SectionAxisDialogManager } from './managers/section-axis-dialog-manager'; import { SectionAxisDialogManager } from './managers/section-axis-dialog-manager';
import { SectionBoxDialogManager } from './managers/section-box-dialog-manager'; import { SectionBoxDialogManager } from './managers/section-box-dialog-manager';
import { WalkControlManager } from './managers/walk-control-manager';
import { EngineInfoDialogManager } from './managers/engine-info-dialog-manager'; import { EngineInfoDialogManager } from './managers/engine-info-dialog-manager';
import { SettingDialogManager } from './managers/setting-dialog-manager'; import { SettingDialogManager } from './managers/setting-dialog-manager';
import { ComponentDetailManager } from './managers/component-detail-manager'; import { ComponentDetailManager } from './managers/component-detail-manager';
@@ -55,7 +54,6 @@ export class CusBimEngine {
public sectionPlane: SectionPlaneDialogManager | null = null; public sectionPlane: SectionPlaneDialogManager | null = null;
public sectionAxis: SectionAxisDialogManager | null = null; public sectionAxis: SectionAxisDialogManager | null = null;
public sectionBox: SectionBoxDialogManager | null = null; public sectionBox: SectionBoxDialogManager | null = null;
public walkControl: WalkControlManager | null = null;
public engineInfo: EngineInfoDialogManager | null = null; public engineInfo: EngineInfoDialogManager | null = null;
public componentDetail: ComponentDetailManager | null = null; public componentDetail: ComponentDetailManager | null = null;
public aiChat: AiChatManager | null = null; public aiChat: AiChatManager | null = null;
@@ -168,8 +166,6 @@ export class CusBimEngine {
this.sectionPlane = new SectionPlaneDialogManager(this.registry); this.sectionPlane = new SectionPlaneDialogManager(this.registry);
this.sectionAxis = new SectionAxisDialogManager(this.registry); this.sectionAxis = new SectionAxisDialogManager(this.registry);
this.sectionBox = new SectionBoxDialogManager(this.registry); this.sectionBox = new SectionBoxDialogManager(this.registry);
this.walkControl = new WalkControlManager(this.registry);
this.walkControl.init();
this.engineInfo = new EngineInfoDialogManager(this.registry); this.engineInfo = new EngineInfoDialogManager(this.registry);
this.engineInfo.init(); this.engineInfo.init();
@@ -179,7 +175,6 @@ export class CusBimEngine {
this.registry.sectionPlane = this.sectionPlane; this.registry.sectionPlane = this.sectionPlane;
this.registry.sectionAxis = this.sectionAxis; this.registry.sectionAxis = this.sectionAxis;
this.registry.sectionBox = this.sectionBox; this.registry.sectionBox = this.sectionBox;
this.registry.walkControl = this.walkControl;
this.registry.engineInfo = this.engineInfo; this.registry.engineInfo = this.engineInfo;
@@ -275,7 +270,6 @@ export class CusBimEngine {
this.sectionPlane?.destroy(); this.sectionPlane?.destroy();
this.sectionAxis?.destroy(); this.sectionAxis?.destroy();
this.sectionBox?.destroy(); this.sectionBox?.destroy();
this.walkControl?.destroy();
this.aiChat?.destroy(); this.aiChat?.destroy();
this.setting?.destroy(); this.setting?.destroy();

View File

@@ -51,10 +51,54 @@ export const enUS: TranslationDictionary = {
}, },
tree: { tree: {
searchPlaceholder: 'Please enter content to search', searchPlaceholder: 'Please enter content to search',
noResults: 'No matching results',
}, },
constructTree: { constructTree: {
title: 'Construct Tree', title: 'Construct Tree',
}, },
componentTreeDrawer: {
actions: {
close: 'Close drawer',
add: 'Add',
addPin: 'Add Pin',
addFolder: 'Add Folder',
edit: 'Edit',
delete: 'Delete',
save: 'Save',
cancel: 'Cancel'
},
tabs: {
view: 'View',
pin: 'Pins',
constructTree: 'Component Tree'
},
empty: {
pinTitle: 'No pins yet',
pinDesc: 'Click the button above to add your first pin'
},
dialogs: {
noticeTitle: 'Notice',
deleteFolderTitle: 'Delete Folder',
deleteFolderMessage: 'Delete this folder and all pins inside it?',
deleteFolderMessagePrefix: 'Delete folder "',
deleteFolderMessageSuffix: '" and all pin data inside it?',
confirm: 'Confirm'
},
placeholders: {
view: 'View page placeholder. View-related content will be added here later.',
pin: 'No pins yet. Click the button above to add one.',
constructTree: 'Component tree placeholder. The new tree capability will be added here later.'
}
},
viewTab: {
category: {
ceilingPlans: 'Ceiling Plans',
elevations: 'Elevations',
floorPlans: 'Floor Plans',
sections: 'Sections',
},
empty: 'No view data available',
},
tab: { tab: {
component: 'Component', component: 'Component',
system: 'System', system: 'System',
@@ -223,6 +267,14 @@ export const enUS: TranslationDictionary = {
otherPlaceholder: 'Enter custom answer', otherPlaceholder: 'Enter custom answer',
submit: 'Submit' submit: 'Submit'
}, },
viewDropdown: {
triggerTitle: 'View Controls',
switchOrthographic: 'Switch to Orthographic',
switchPerspective: 'Switch to Perspective',
saveMainView: 'Set Current as Main View',
restoreMainView: 'Reset Main View',
captureScreenshot: 'Capture Screenshot',
},
setting: { setting: {
dialogTitle: 'Settings', dialogTitle: 'Settings',
presetSelect: 'Preset', presetSelect: 'Preset',
@@ -272,5 +324,58 @@ export const enUS: TranslationDictionary = {
ground: 'Ground', ground: 'Ground',
groundElevation: 'Ground Elevation', groundElevation: 'Ground Elevation',
groundElevationUnit: 'm', groundElevationUnit: 'm',
},
help: {
engineControl: 'Engine Controls',
fpsControl: 'First Person Controls',
gotIt: 'Got it',
dontRemind: 'Don\'t remind me again',
rotate: 'Rotate',
mouseLeftDrag: 'Left mouse drag',
pan: 'Pan',
shiftLeftDrag: 'Shift + Left/Right drag',
zoom: 'Zoom',
mouseWheel: 'Mouse wheel',
boxSelect: 'Box Select',
ctrlLeftDrag: 'Ctrl + Left drag',
multiSelect: 'Add/Remove Selection',
ctrlShiftClick: 'Ctrl + Click (Add) / Shift + Click (Remove)',
homeView: 'Home View',
doubleClickBlank: 'Double click empty area',
rotateRuleTitle: 'Rotation Center Rule',
rotateRuleDesc: 'Hovering over a component sets the rotation center to that position; hovering over empty area keeps the last rotation center.',
roam: 'Roam',
or: 'or',
upDown: 'Move Up/Down',
qeKey: 'Q / E',
landOnFloor: 'Enable gravity to land on nearest floor',
run: 'Run (Speed up)',
shiftArrow: 'Shift + Arrow keys',
teleport: 'Teleport',
doubleClick: 'Double click target',
lookAround: 'Look Around',
leftDrag: 'Drag view with left button',
adjustSpeed: 'Adjust Speed',
rotateView: 'Rotate View',
panView: 'Pan View',
zoomView: 'Zoom View',
resetView: 'Reset View',
mouseLeftDragShort: 'Left mouse drag',
mouseRightDragShort: 'Right mouse drag',
mouseWheelShort: 'Scroll mouse wheel',
componentSelect: 'Component Selection',
click: 'Click',
addSelect: 'Add to selection',
removeSelect: 'Remove from selection',
doubleClickBlankShort: 'Double click empty area',
pointToComponent: 'Point to component:',
pointToComponentDesc: 'Set rotation center at click position',
pointToBlank: 'Point to blank:',
pointToBlankDesc: 'Keep last rotation center',
roamMove: 'Roam Move',
roamMoveDesc: 'Use WASD or arrow keys to move',
speedUp: 'Speed Up',
lookAroundDesc: 'Drag view with left button',
teleportDesc: 'Double click target component or ground',
} }
}; };

View File

@@ -74,10 +74,54 @@ export interface TranslationDictionary {
}; };
tree: { tree: {
searchPlaceholder: string; searchPlaceholder: string;
noResults: string;
}; };
constructTree: { constructTree: {
title: string; title: string;
}; };
componentTreeDrawer: {
actions: {
close: string;
add: string;
addPin: string;
addFolder: string;
edit: string;
delete: string;
save: string;
cancel: string;
};
tabs: {
view: string;
pin: string;
constructTree: string;
};
empty: {
pinTitle: string;
pinDesc: string;
};
dialogs: {
noticeTitle: string;
deleteFolderTitle: string;
deleteFolderMessage: string;
deleteFolderMessagePrefix: string;
deleteFolderMessageSuffix: string;
confirm: string;
};
placeholders: {
view: string;
pin: string;
constructTree: string;
};
};
viewTab: {
category: {
ceilingPlans: string;
elevations: string;
floorPlans: string;
sections: string;
};
empty: string;
};
tab: { tab: {
component: string; component: string;
system: string; system: string;
@@ -244,6 +288,14 @@ export interface TranslationDictionary {
otherPlaceholder: string; otherPlaceholder: string;
submit: string; submit: string;
}; };
viewDropdown: {
triggerTitle: string;
switchOrthographic: string;
switchPerspective: string;
saveMainView: string;
restoreMainView: string;
captureScreenshot: string;
};
setting: { setting: {
dialogTitle: string; dialogTitle: string;
presetSelect: string; presetSelect: string;
@@ -304,6 +356,59 @@ export interface TranslationDictionary {
sky: string; sky: string;
}; };
}; };
help: {
engineControl: string;
fpsControl: string;
gotIt: string;
dontRemind: string;
rotate: string;
mouseLeftDrag: string;
pan: string;
shiftLeftDrag: string;
zoom: string;
mouseWheel: string;
boxSelect: string;
ctrlLeftDrag: string;
multiSelect: string;
ctrlShiftClick: string;
homeView: string;
doubleClickBlank: string;
rotateRuleTitle: string;
rotateRuleDesc: string;
roam: string;
or: string;
upDown: string;
qeKey: string;
landOnFloor: string;
run: string;
shiftArrow: string;
teleport: string;
doubleClick: string;
lookAround: string;
leftDrag: string;
adjustSpeed: string;
rotateView: string;
panView: string;
zoomView: string;
resetView: string;
mouseLeftDragShort: string;
mouseRightDragShort: string;
mouseWheelShort: string;
componentSelect: string;
click: string;
addSelect: string;
removeSelect: string;
doubleClickBlankShort: string;
pointToComponent: string;
pointToComponentDesc: string;
pointToBlank: string;
pointToBlankDesc: string;
roamMove: string;
roamMoveDesc: string;
speedUp: string;
lookAroundDesc: string;
teleportDesc: string;
};
} }
/** /**

View File

@@ -51,10 +51,54 @@ export const zhCN: TranslationDictionary = {
}, },
tree: { tree: {
searchPlaceholder: '请输入要搜索的内容', searchPlaceholder: '请输入要搜索的内容',
noResults: '未找到匹配结果',
}, },
constructTree: { constructTree: {
title: '目录树', title: '目录树',
}, },
componentTreeDrawer: {
actions: {
close: '关闭抽屉',
add: '新增',
addPin: '新增图钉',
addFolder: '新增文件夹',
edit: '编辑',
delete: '删除',
save: '保存',
cancel: '取消'
},
tabs: {
view: '视图',
pin: '图钉',
constructTree: '构件树'
},
empty: {
pinTitle: '暂无图钉',
pinDesc: '点击上方按钮添加第一个图钉'
},
dialogs: {
noticeTitle: '提示',
deleteFolderTitle: '删除文件夹',
deleteFolderMessage: '确认删除该文件夹以及文件夹下面的所有图钉吗?',
deleteFolderMessagePrefix: '是否删除【',
deleteFolderMessageSuffix: '】文件夹以及文件夹下面所有的图钉数据。',
confirm: '确定'
},
placeholders: {
view: '视图页面占位,后续在这里接入视图相关内容。',
pin: '暂无图钉,点击上方按钮添加',
constructTree: '构件树页面占位,后续在这里接入新的构件树能力。'
}
},
viewTab: {
category: {
ceilingPlans: '天花板平面',
elevations: '立面',
floorPlans: '楼层平面',
sections: '剖面',
},
empty: '暂无视图数据',
},
tab: { tab: {
component: '构件', component: '构件',
system: '系统', system: '系统',
@@ -223,6 +267,14 @@ export const zhCN: TranslationDictionary = {
otherPlaceholder: '请输入自定义答案', otherPlaceholder: '请输入自定义答案',
submit: '提交' submit: '提交'
}, },
viewDropdown: {
triggerTitle: '视图控制',
switchOrthographic: '切换到正交视图',
switchPerspective: '切换到透视视图',
saveMainView: '将当前视图设为主视图',
restoreMainView: '重置主视图',
captureScreenshot: '截图保存',
},
setting: { setting: {
dialogTitle: '设置', dialogTitle: '设置',
presetSelect: '选择预设', presetSelect: '选择预设',
@@ -272,5 +324,58 @@ export const zhCN: TranslationDictionary = {
ground: '显示地面', ground: '显示地面',
groundElevation: '地面高度', groundElevation: '地面高度',
groundElevationUnit: 'm', groundElevationUnit: 'm',
},
help: {
engineControl: '引擎控制',
fpsControl: '第一视角控制',
gotIt: '好的,知道了',
dontRemind: '不再提醒我',
rotate: '旋转',
mouseLeftDrag: '鼠标左键拖拽',
pan: '平移',
shiftLeftDrag: 'Shift + 左键 / 右键拖拽',
zoom: '缩放',
mouseWheel: '鼠标滚轮',
boxSelect: '框选',
ctrlLeftDrag: 'Ctrl + 左键拖拽',
multiSelect: '加选 / 减选',
ctrlShiftClick: 'Ctrl + 单击 (加) / Shift + 单击 (减)',
homeView: '回到主视图',
doubleClickBlank: '双击空白区域',
rotateRuleTitle: '旋转点变换规则',
rotateRuleDesc: '鼠标悬停构件时,按当前位置设定旋转中心;悬停空白区域时,保持上一次旋转中心。',
roam: '漫游',
or: '或',
upDown: '向上和向下移动',
qeKey: 'Q / E',
landOnFloor: '开启重力将落在最近楼层',
run: '运行 (加速)',
shiftArrow: 'Shift + 方向键',
teleport: '传送',
doubleClick: '双击目标',
lookAround: '环视',
leftDrag: '使用左键拖动视图',
adjustSpeed: '调整速度',
rotateView: '旋转视图',
panView: '平移视图',
zoomView: '缩放视图',
resetView: '视图复位',
mouseLeftDragShort: '鼠标左键拖拽',
mouseRightDragShort: '鼠标右键拖拽',
mouseWheelShort: '滚动鼠标滚轮',
componentSelect: '构件选择',
click: '单击',
addSelect: '加选构件',
removeSelect: '减选构件',
doubleClickBlankShort: '双击空白区域',
pointToComponent: '指向构件:',
pointToComponentDesc: '以鼠标点击位置为旋转中心',
pointToBlank: '指向空白:',
pointToBlankDesc: '保持上一次确定的旋转中心',
roamMove: '漫游移动',
roamMoveDesc: '使用 WASD 或方向键进行移动',
speedUp: '加速移动',
lookAroundDesc: '左键拖动视图查看四周',
teleportDesc: '双击目标构件或地面',
} }
}; };

View File

@@ -51,10 +51,54 @@ export const zhTW: TranslationDictionary = {
}, },
tree: { tree: {
searchPlaceholder: '請輸入要搜尋的內容', searchPlaceholder: '請輸入要搜尋的內容',
noResults: '未找到符合結果',
}, },
constructTree: { constructTree: {
title: '目錄樹', title: '目錄樹',
}, },
componentTreeDrawer: {
actions: {
close: '關閉抽屜',
add: '新增',
addPin: '新增圖釘',
addFolder: '新增文件夾',
edit: '編輯',
delete: '刪除',
save: '保存',
cancel: '取消'
},
tabs: {
view: '視圖',
pin: '圖釘',
constructTree: '構件樹'
},
empty: {
pinTitle: '暫無圖釘',
pinDesc: '點擊上方按鈕添加第一個圖釘'
},
dialogs: {
noticeTitle: '提示',
deleteFolderTitle: '刪除文件夾',
deleteFolderMessage: '確認刪除該文件夾以及文件夾下面的所有圖釘嗎?',
deleteFolderMessagePrefix: '是否刪除【',
deleteFolderMessageSuffix: '】文件夾以及文件夾下面所有的圖釘資料。',
confirm: '確定'
},
placeholders: {
view: '視圖頁面占位,後續會在這裡接入視圖相關內容。',
pin: '暫無圖釘,點擊上方按鈕添加',
constructTree: '構件樹頁面占位,後續會在這裡接入新的構件樹能力。'
}
},
viewTab: {
category: {
ceilingPlans: '天花板平面',
elevations: '立面',
floorPlans: '樓層平面',
sections: '剖面',
},
empty: '暫無視圖資料',
},
tab: { tab: {
component: '構件', component: '構件',
system: '系統', system: '系統',
@@ -223,6 +267,14 @@ export const zhTW: TranslationDictionary = {
otherPlaceholder: '請輸入自定義答案', otherPlaceholder: '請輸入自定義答案',
submit: '提交' submit: '提交'
}, },
viewDropdown: {
triggerTitle: '視圖控制',
switchOrthographic: '切換到正交視圖',
switchPerspective: '切換到透視視圖',
saveMainView: '將當前視圖設為主視圖',
restoreMainView: '重置主視圖',
captureScreenshot: '截圖保存',
},
setting: { setting: {
dialogTitle: '設定', dialogTitle: '設定',
presetSelect: '選擇預設', presetSelect: '選擇預設',
@@ -272,5 +324,58 @@ export const zhTW: TranslationDictionary = {
ground: '顯示地面', ground: '顯示地面',
groundElevation: '地面高度', groundElevation: '地面高度',
groundElevationUnit: 'm', groundElevationUnit: 'm',
},
help: {
engineControl: '引擎控制',
fpsControl: '第一人稱控制',
gotIt: '好的,知道了',
dontRemind: '不再提醒我',
rotate: '旋轉',
mouseLeftDrag: '滑鼠左鍵拖曳',
pan: '平移',
shiftLeftDrag: 'Shift + 左鍵 / 右鍵拖曳',
zoom: '縮放',
mouseWheel: '滑鼠滾輪',
boxSelect: '框選',
ctrlLeftDrag: 'Ctrl + 左鍵拖曳',
multiSelect: '加選 / 減選',
ctrlShiftClick: 'Ctrl + 點擊 (加) / Shift + 點擊 (減)',
homeView: '回到主視圖',
doubleClickBlank: '雙擊空白區域',
rotateRuleTitle: '旋轉點變換規則',
rotateRuleDesc: '滑鼠懸停構件時,按當前位置設定旋轉中心;懸停空白區域時,保持上一次旋轉中心。',
roam: '漫遊',
or: '或',
upDown: '向上和向下移動',
qeKey: 'Q / E',
landOnFloor: '開啟重力將落在最近樓層',
run: '運行 (加速)',
shiftArrow: 'Shift + 方向鍵',
teleport: '傳送',
doubleClick: '雙擊目標',
lookAround: '環視',
leftDrag: '使用左鍵拖動視圖',
adjustSpeed: '調整速度',
rotateView: '旋轉視圖',
panView: '平移視圖',
zoomView: '縮放視圖',
resetView: '視圖復位',
mouseLeftDragShort: '滑鼠左鍵拖曳',
mouseRightDragShort: '滑鼠右鍵拖曳',
mouseWheelShort: '滾動滑鼠滾輪',
componentSelect: '構件選擇',
click: '點擊',
addSelect: '加選構件',
removeSelect: '減選構件',
doubleClickBlankShort: '雙擊空白區域',
pointToComponent: '指向構件:',
pointToComponentDesc: '以滑鼠點擊位置為旋轉中心',
pointToBlank: '指向空白:',
pointToBlankDesc: '保持上一次確定的旋轉中心',
roamMove: '漫遊移動',
roamMoveDesc: '使用 WASD 或方向鍵進行移動',
speedUp: '加速移動',
lookAroundDesc: '左鍵拖動視圖查看四周',
teleportDesc: '雙擊目標構件或地面',
} }
}; };

View File

@@ -152,6 +152,8 @@ export class ComponentDetailManager extends BaseManager {
const row = document.createElement('div'); const row = document.createElement('div');
row.style.cssText = ` row.style.cssText = `
display: flex; display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--bim-border-default, rgba(255,255,255,0.15)); border-bottom: 1px solid var(--bim-border-default, rgba(255,255,255,0.15));
`; `;
@@ -163,22 +165,37 @@ export class ComponentDetailManager extends BaseManager {
label.style.cssText = ` label.style.cssText = `
width: 120px; width: 120px;
flex-shrink: 0; flex-shrink: 0;
padding: 8px 12px;
`;
const labelText = document.createElement('div');
labelText.style.cssText = `
color: var(--bim-text-secondary, #999); color: var(--bim-text-secondary, #999);
font-size: 13px; font-size: 13px;
padding: 8px 12px; font-weight: 600;
border-right: 1px solid var(--bim-border-default, rgba(255,255,255,0.15)); overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`; `;
label.textContent = item.name || '-'; labelText.textContent = item.name || '-';
label.appendChild(labelText);
const value = document.createElement('div'); const value = document.createElement('div');
value.style.cssText = ` value.style.cssText = `
flex: 1; flex: 1;
min-width: 0;
padding: 8px 12px;
`;
const valueText = document.createElement('div');
valueText.style.cssText = `
color: var(--bim-text-primary, #fff); color: var(--bim-text-primary, #fff);
font-size: 13px; font-size: 13px;
padding: 8px 12px; overflow: hidden;
word-break: break-all; text-overflow: ellipsis;
white-space: nowrap;
`; `;
value.textContent = String(item.value ?? '-'); const valueContent = String(item.value ?? '-');
valueText.textContent = valueContent;
value.appendChild(valueText);
row.appendChild(label); row.appendChild(label);
row.appendChild(value); row.appendChild(value);

View File

@@ -0,0 +1,67 @@
import { ComponentTreeDrawer } from '../components/component-tree-drawer';
import type { ComponentTreeDrawerTabApi, ComponentTreeDrawerTabId } from '../components/component-tree-drawer/types';
import { BaseManager } from '../core/base-manager';
import { ManagerRegistry } from '../core/manager-registry';
export class ComponentTreeDrawerManager extends BaseManager {
private drawer: ComponentTreeDrawer | null = null;
constructor(registry: ManagerRegistry) {
super(registry);
this.init();
}
public init(): void {
if (this.drawer || !this.registry.wrapper) {
return;
}
this.drawer = new ComponentTreeDrawer({
container: this.registry.wrapper,
registry: this.registry
});
this.drawer.init();
}
public open(tabId?: ComponentTreeDrawerTabId): void {
this.drawer?.openDrawer(tabId);
}
public close(): void {
this.drawer?.close();
}
public toggle(tabId?: ComponentTreeDrawerTabId): void {
this.drawer?.toggle(tabId);
}
public switchTab(tabId: ComponentTreeDrawerTabId): void {
this.drawer?.switchTab(tabId);
}
public isOpen(): boolean {
return this.drawer?.isOpen() ?? false;
}
public getActiveTab(): ComponentTreeDrawerTabId {
return this.drawer?.getActiveTab() ?? 'construct-tree';
}
public getViewTabApi(): ComponentTreeDrawerTabApi | null {
return this.drawer?.getViewTabApi() ?? null;
}
public getPinTabApi(): ComponentTreeDrawerTabApi | null {
return this.drawer?.getPinTabApi() ?? null;
}
public getConstructTreeTabApi(): ComponentTreeDrawerTabApi | null {
return this.drawer?.getConstructTreeTabApi() ?? null;
}
public destroy(): void {
this.drawer?.destroy();
this.drawer = null;
super.destroy();
}
}

View File

@@ -312,6 +312,10 @@ export class ConstructTreeManagerBtn extends BaseManager {
label: 'construct-tree', label: 'construct-tree',
icon: getIcon('目录树'), icon: getIcon('目录树'),
onClick: () => { onClick: () => {
if (this.registry.componentTreeDrawer) {
this.registry.componentTreeDrawer.toggle();
return;
}
this.openConstructTreeDialog(); this.openConstructTreeDialog();
} }
}); });
@@ -330,6 +334,11 @@ export class ConstructTreeManagerBtn extends BaseManager {
* 6. 创建对话框并显示 * 6. 创建对话框并显示
*/ */
public async openConstructTreeDialog() { public async openConstructTreeDialog() {
if (this.registry.componentTreeDrawer) {
this.registry.componentTreeDrawer.open();
return;
}
// 隐藏按钮组,避免遮挡对话框 // 隐藏按钮组,避免遮挡对话框
this.setVisible(false); this.setVisible(false);

View File

@@ -12,6 +12,7 @@ import { BaseManager } from '../core/base-manager';
import { RightKeyManager } from './right-key-manager'; import { RightKeyManager } from './right-key-manager';
import type { MenuItemConfig } from '../components/menu/item'; import type { MenuItemConfig } from '../components/menu/item';
import { ManagerRegistry } from '../core/manager-registry'; import { ManagerRegistry } from '../core/manager-registry';
import type { DrawingPinRecord } from '../types/events';
/** /**
* 3D 引擎管理器 * 3D 引擎管理器
@@ -302,6 +303,14 @@ export class EngineManager extends BaseManager {
return this.engineInstance.jumpToCamera(cameraData); return this.engineInstance.jumpToCamera(cameraData);
} }
public setMainViewPort(viewData: any): void {
if (!this.engineInstance) {
console.warn('[EngineManager] 3D Engine not initialized.');
return;
}
this.engineInstance.setMainViewPort(viewData);
}
public getConstructTreeData(): { level: any[]; type: any[]; major: any[] } { public getConstructTreeData(): { level: any[]; type: any[]; major: any[] } {
if (!this.engineInstance) { if (!this.engineInstance) {
console.warn('[EngineManager] 3D Engine not initialized.'); console.warn('[EngineManager] 3D Engine not initialized.');
@@ -322,6 +331,19 @@ export class EngineManager extends BaseManager {
this.engineInstance.getComponentProperties(url, id, callback); this.engineInstance.getComponentProperties(url, id, callback);
} }
public setPinRecords(records: DrawingPinRecord[]): void {
console.log('[EngineManager] setPinRecords called, records count:', records.length);
if (!this.engineInstance) {
console.warn('[EngineManager] 3D Engine not initialized.');
return;
}
this.engineInstance.setPinRecords(records);
}
public setPinList(records: DrawingPinRecord[]): void {
this.setPinRecords(records);
}
public registerRightKeyHandler(handler: (e: MouseEvent) => MenuItemConfig[] | null): void { public registerRightKeyHandler(handler: (e: MouseEvent) => MenuItemConfig[] | null): void {
if (!this.rightKey) { if (!this.rightKey) {
console.warn('[EngineManager] RightKey manager not initialized.'); console.warn('[EngineManager] RightKey manager not initialized.');

View File

@@ -71,6 +71,7 @@ export class MeasureDialogManager extends BaseDialogManager {
}, },
}); });
this.panel.init(); this.panel.init();
this.engineComponent?.activateMeasure('distance');
this.config = this.panel.getConfig(); this.config = this.panel.getConfig();
if (this.config) { if (this.config) {
@@ -103,11 +104,18 @@ export class MeasureDialogManager extends BaseDialogManager {
this.handleMeasureChanged(data); this.handleMeasureChanged(data);
} }
}; };
const quitHandler = () => {
console.log('[MeasureDialogManager] quit_measure_draw received');
this.panel?.clearActiveMode();
this.engineComponent?.resetMeasureState();
};
ec.onRawEvent('measure-changed', handler); ec.onRawEvent('measure-changed', handler);
ec.onRawEvent('measure-click', handler); ec.onRawEvent('measure-click', handler);
ec.onRawEvent('quit_measure_draw', quitHandler);
this.unsubscribeMeasureChanged = () => { this.unsubscribeMeasureChanged = () => {
ec.offRawEvent('measure-changed', handler); ec.offRawEvent('measure-changed', handler);
ec.offRawEvent('measure-click', handler); ec.offRawEvent('measure-click', handler);
ec.offRawEvent('quit_measure_draw', quitHandler);
} }
} }
} }

View File

@@ -99,6 +99,7 @@ export class MeasureDockManager extends BaseManager {
}); });
this.panel.init(); this.panel.init();
this.panel.switchMode('distance'); this.panel.switchMode('distance');
this.engineComponent?.activateMeasure('distance');
this.engineComponent?.setClearHeightDirection(1); this.engineComponent?.setClearHeightDirection(1);
this.engineComponent?.setClearHeightSelectType('point'); this.engineComponent?.setClearHeightSelectType('point');
const config = this.panel.getConfig(); const config = this.panel.getConfig();
@@ -108,6 +109,7 @@ export class MeasureDockManager extends BaseManager {
}); });
} else { } else {
this.panel.switchMode('distance'); this.panel.switchMode('distance');
this.engineComponent?.activateMeasure('distance');
} }
this.applyPresentation(); this.applyPresentation();
return this.panel.element; return this.panel.element;
@@ -130,12 +132,19 @@ export class MeasureDockManager extends BaseManager {
const clickHandler = (data: EngineMeasureData) => { const clickHandler = (data: EngineMeasureData) => {
this.handleMeasureCallback('measure-click', data); this.handleMeasureCallback('measure-click', data);
}; };
const quitHandler = () => {
console.log('[MeasureDockManager] quit_measure_draw received');
this.panel?.clearActiveMode();
this.engineComponent?.resetMeasureState();
};
ec.onRawEvent('measure-changed', changedHandler); ec.onRawEvent('measure-changed', changedHandler);
ec.onRawEvent('measure-click', clickHandler); ec.onRawEvent('measure-click', clickHandler);
ec.onRawEvent('quit_measure_draw', quitHandler);
this.unsubscribeMeasureEvents = () => { this.unsubscribeMeasureEvents = () => {
ec.offRawEvent('measure-changed', changedHandler); ec.offRawEvent('measure-changed', changedHandler);
ec.offRawEvent('measure-click', clickHandler); ec.offRawEvent('measure-click', clickHandler);
ec.offRawEvent('quit_measure_draw', quitHandler);
}; };
console.log('[MeasureDockManager] raw event callbacks bound'); console.log('[MeasureDockManager] raw event callbacks bound');

View File

@@ -1,145 +0,0 @@
/**
* 漫游控制管理器
* 负责管理漫游模式的控制面板和相关交互
*/
import { BaseManager } from '../core/base-manager';
import { ManagerRegistry } from '../core/manager-registry';
import { WalkControlPanel } from '../components/walk-control-panel';
import { WalkPathDialogManager } from './walk-path-dialog-manager';
/**
* 漫游控制管理器
* 提供第一人称漫游、路径漫游等功能的控制界面
*/
export class WalkControlManager extends BaseManager {
/** 漫游控制面板实例 */
public panel: WalkControlPanel | null = null;
/** 路径漫游对话框管理器 */
private pathManager: WalkPathDialogManager | null = null;
constructor(registry: ManagerRegistry) {
super(registry);
}
/** 初始化管理器 */
public init(): void {
this.pathManager = new WalkPathDialogManager(this.registry);
this.pathManager.init();
}
/** 显示漫游控制面板 */
public show(): void {
this.registry.toolbar?.hide();
// 打开漫游面板时,默认激活第一人称模式
console.log('[WalkControl] 打开漫游面板,激活第一人称模式');
this.engineComponent?.activateFirstPersonMode();
const engineSpeed = this.engineComponent?.getWalkSpeed() ?? 1;
const panelSpeed = Math.round(engineSpeed / 0.1);
console.log('[WalkControl] 初始速度 - engineSpeed:', engineSpeed, 'panelSpeed:', panelSpeed);
this.panel = new WalkControlPanel({
defaultSpeed: panelSpeed,
onPlanViewToggle: (isActive) => {
console.log('[WalkControl] 小地图:', isActive);
this.engineComponent?.toggleMiniMap();
// 同步工具栏地图按钮的激活状态
this.registry.toolbar?.setBtnActive('map', isActive);
this.emit('walk:plan-view-toggle', { isActive });
},
onPathModeToggle: (isActive) => {
console.log('[WalkControl] 路径漫游:', isActive);
if (isActive) {
this.pathManager?.show();
} else {
this.pathManager?.hide();
}
this.emit('walk:path-mode-toggle', { isActive });
},
onWalkModeToggle: (isActive) => {
console.log('[WalkControl] 第三人称漫游按钮点击:', isActive);
if (isActive) {
this.pathManager?.hide();
alert('第三人称功能开发中');
}
this.emit('walk:walk-mode-toggle', { isActive });
},
onSpeedChange: (speed) => {
console.log('[WalkControl] 速度变化:', speed);
const engineSpeed = speed * 0.1;
this.engineComponent?.setWalkSpeed(engineSpeed);
this.emit('walk:speed-change', { speed });
},
onGravityToggle: (enabled) => {
console.log('[WalkControl] 重力:', enabled);
this.engineComponent?.setWalkGravity(enabled);
this.emit('walk:gravity-toggle', { enabled });
},
onCollisionToggle: (enabled) => {
console.log('[WalkControl] 碰撞:', enabled);
this.engineComponent?.setWalkCollision(enabled);
this.emit('walk:collision-toggle', { enabled });
},
onCharacterModelChange: (model) => {
console.log('[WalkControl] 角色模型:', model);
},
onWalkModeChange: (mode) => {
console.log('[WalkControl] 行走模式:', mode);
},
onExit: () => {
this.hide();
}
});
this.panel.init();
// 同步当前地图状态到漫游面板
const mapState = this.engineComponent?.getMiniMapState() ?? false;
this.panel.setPlanViewActive(mapState);
if (this.registry.container) {
this.panel.element.style.position = 'absolute';
this.panel.element.style.bottom = '20px';
this.panel.element.style.left = '50%';
this.panel.element.style.transform = 'translateX(-50%)';
this.panel.element.style.zIndex = '1000';
this.registry.container.appendChild(this.panel.element);
} else {
console.warn('[WalkControlManager] Container not found');
}
}
/** 隐藏漫游控制面板 */
public hide(): void {
this.pathManager?.hide();
// 如果小地图开着,先关闭它
if (this.engineComponent?.getMiniMapState()) {
this.engineComponent.toggleMiniMap();
}
console.log('[WalkControl] 关闭漫游面板,退出第一人称模式');
this.engineComponent?.deactivateFirstPersonMode();
if (this.panel) {
this.panel.destroy();
this.panel = null;
}
// 同步工具栏地图按钮的激活状态
this.registry.toolbar?.setBtnActive('map', false);
if (this.registry.toolbar) {
this.registry.toolbar.show();
}
}
/** 销毁管理器 */
public destroy(): void {
this.hide();
this.pathManager?.destroy();
this.pathManager = null;
super.destroy();
}
}

View File

@@ -67,8 +67,7 @@ export class WalkDockManager extends BaseManager {
this.emit('walk:walk-mode-toggle', { isActive }); this.emit('walk:walk-mode-toggle', { isActive });
}, },
onSpeedChange: (speed) => { onSpeedChange: (speed) => {
const engineSpeed = speed * 0.1; this.engineComponent?.setWalkSpeed(speed);
this.engineComponent?.setWalkSpeed(engineSpeed);
this.emit('walk:speed-change', { speed }); this.emit('walk:speed-change', { speed });
}, },
onGravityToggle: (enabled) => { onGravityToggle: (enabled) => {
@@ -96,14 +95,25 @@ export class WalkDockManager extends BaseManager {
return this.panel.element; return this.panel.element;
} }
public setPlanViewActive(active: boolean): void {
this.panel?.setPlanViewActive(active);
}
public setPathModeActive(active: boolean): void {
this.panel?.setPathModeActive(active);
}
private onOpen(): void { private onOpen(): void {
this.engineComponent?.activateFirstPersonMode(); this.engineComponent?.activateFirstPersonMode();
this.syncMapState(); this.syncMapState();
this.engineComponent?.setWalkSpeed(5)
const engineSpeed = this.engineComponent?.getWalkSpeed() ?? 1; const engineSpeed = this.engineComponent?.getWalkSpeed() ?? 1;
const panelSpeed = Math.round(engineSpeed / 0.1); const engineGravity = this.engineComponent?.getWalkGravity() ?? false;
console.log('[WalkDock] 初始速度 - engineSpeed:', engineSpeed, 'panelSpeed:', panelSpeed); const engineCollision = this.engineComponent?.getWalkCollision() ?? false;
this.panel?.setSpeed(panelSpeed); console.log('[WalkDock] 初始状态 - speed:', engineSpeed, 'gravity:', engineGravity, 'collision:', engineCollision);
this.panel?.setSpeed(engineSpeed);
this.panel?.setGravity(engineGravity);
this.panel?.setCollision(engineCollision);
} }
private onClose(): void { private onClose(): void {

View File

@@ -57,9 +57,7 @@ export class WalkPathDialogManager extends BaseDialogManager {
/** 对话框关闭时的回调 */ /** 对话框关闭时的回调 */
protected onDialogClose(): void { protected onDialogClose(): void {
if (this.registry.walkControl && this.registry.walkControl.panel) { this.registry.walkDock?.setPathModeActive(false);
this.registry.walkControl.panel.setPathModeActive(false);
}
} }
/** 销毁前的清理 */ /** 销毁前的清理 */

View File

@@ -57,9 +57,7 @@ export class WalkPlanViewDialogManager extends BaseDialogManager {
/** 对话框关闭时的回调 */ /** 对话框关闭时的回调 */
protected onDialogClose(): void { protected onDialogClose(): void {
if (this.registry.walkControl && this.registry.walkControl.panel) { this.registry.walkDock?.setPlanViewActive(false);
this.registry.walkControl.panel.setPlanViewActive(false);
}
} }
/** 销毁前的清理 */ /** 销毁前的清理 */

View File

@@ -1,5 +1,31 @@
import type { EngineSettingPreset, EngineSettings } from '../components/engine/types'; import type { EngineSettingPreset, EngineSettings } from '../components/engine/types';
export interface DrawingPinRecord {
id: string;
parentId?: string | null;
name: string;
type: 'pin' | 'folder';
seq: number;
data?: any;
}
export interface DrawingPinCreatePayload {
record: DrawingPinRecord;
}
export interface DrawingPinUpdatePayload {
id: string;
patch: Partial<Pick<DrawingPinRecord, 'name' | 'parentId' | 'seq' | 'data'>>;
}
export interface DrawingPinDeletePayload {
id: string;
}
export interface DrawingPinListUpdatedPayload {
records: DrawingPinRecord[];
}
export interface EngineEvents { export interface EngineEvents {
// UI Events // UI Events
'ui:open-dialog': { id: string; data?: any }; 'ui:open-dialog': { id: string; data?: any };
@@ -66,6 +92,16 @@ export interface EngineEvents {
'aiChat:new-chat': {}; 'aiChat:new-chat': {};
'aiChat:history-opened': {}; 'aiChat:history-opened': {};
// 视图事件
'view:main-view-saved': { viewData: any };
'view:main-view-restored': {};
// 图钉事件
'drawingPin:create': DrawingPinCreatePayload;
'drawingPin:update': DrawingPinUpdatePayload;
'drawingPin:delete': DrawingPinDeletePayload;
'drawingPin:list-updated': DrawingPinListUpdatedPayload;
// 2D 引擎事件 // 2D 引擎事件
'engine2d:drawing-loaded': { url: string }; 'engine2d:drawing-loaded': { url: string };
'engine2d:entity-clicked': { data: any }; 'engine2d:entity-clicked': { data: any };

View File

@@ -70,6 +70,12 @@ const ICONS: Record<string, string> = {
: '<svg width="48" height="48" viewBox="0 0 48 48"><path fill="currentColor" d="M24 4L4 14v20l20 10l20-10V14L24 4zm0 4.5l14 7v14l-14 7l-14-7v-14l14-7zM24 18a6 6 0 100 12a6 6 0 000-12z"/></svg>', : '<svg width="48" height="48" viewBox="0 0 48 48"><path fill="currentColor" d="M24 4L4 14v20l20 10l20-10V14L24 4zm0 4.5l14 7v14l-14 7l-14-7v-14l14-7zM24 18a6 6 0 100 12a6 6 0 000-12z"/></svg>',
: '<svg width="48" height="48" viewBox="0 0 48 48"><path fill="currentColor" d="M6 6h36v36H6V6zm4 4v28h28V10H10zm4 4h20v20H14V14z"/></svg>', : '<svg width="48" height="48" viewBox="0 0 48 48"><path fill="currentColor" d="M6 6h36v36H6V6zm4 4v28h28V10H10zm4 4h20v20H14V14z"/></svg>',
doubleArrowDown: '<svg width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/><path fill="currentColor" d="M7.41 14.59L12 19.17l4.59-4.58L18 16l-6 6-6-6 1.41-1.41z"/></svg>',
pin: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m3 21l4.63-4.631m.005-.005l-2.78-2.78c-.954-.953.006-2.996 1.31-3.078c1.178-.075 3.905.352 4.812-.555l2.49-2.49c.617-.618.225-2 .185-2.762c-.058-1.016 1.558-2.271 2.415-1.414l4.647 4.648c.86.858-.4 2.469-1.413 2.415c-.762-.04-2.145-.432-2.763.185l-2.49 2.49c-.906.907-.48 3.633-.554 4.811c-.082 1.305-2.125 2.265-3.08 1.31z"/></svg>',
folder: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M10 4l2 2h8a2 2 0 0 1 2 2v8.5A3.5 3.5 0 0 1 18.5 20h-13A3.5 3.5 0 0 1 2 16.5v-9A3.5 3.5 0 0 1 5.5 4z" opacity="0.2"/><path fill="currentColor" d="M5.5 4A3.5 3.5 0 0 0 2 7.5v9A3.5 3.5 0 0 0 5.5 20h13a3.5 3.5 0 0 0 3.5-3.5V8A2 2 0 0 0 20 6h-8l-2-2zm0 1.5h3.879l2 2H20a.5.5 0 0 1 .5.5v1H3.5v-1.5a2 2 0 0 1 2-2m-2 5h17v6a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2z"/></svg>',
folderPlus: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M10 4l2 2h8a2 2 0 0 1 2 2v1.5h-1.5V8a.5.5 0 0 0-.5-.5h-8.621l-2-2H5.5a2 2 0 0 0-2 2V9H2V7.5A3.5 3.5 0 0 1 5.5 4z"/><path fill="currentColor" d="M5.5 10A3.5 3.5 0 0 0 2 13.5v3A3.5 3.5 0 0 0 5.5 20H12v-1.5H5.5a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2H12V10z" opacity="0.4"/><path fill="currentColor" d="M17.25 12v3.25H14v1.5h3.25V20h1.5v-3.25H22v-1.5h-3.25V12z"/></svg>',
// ========== 默认图标 ========== // ========== 默认图标 ==========
default: '<svg width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/></svg>', default: '<svg width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/></svg>',
}; };