diff --git a/ScreenShot_2026-05-20_195151_973.png b/ScreenShot_2026-05-20_195151_973.png new file mode 100644 index 0000000..1a9da24 Binary files /dev/null and b/ScreenShot_2026-05-20_195151_973.png differ diff --git a/ScreenShot_2026-05-21_094326_711.png b/ScreenShot_2026-05-21_094326_711.png new file mode 100644 index 0000000..54d68cc Binary files /dev/null and b/ScreenShot_2026-05-21_094326_711.png differ diff --git a/index.html b/index.html index edc927c..6c30271 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,7 @@ body { font-family: Arial, sans-serif; overflow: hidden; - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + background-color: rgb(255, 255, 255); } #app { @@ -336,8 +336,10 @@
- - + + + +
@@ -367,8 +369,6 @@
- -
@@ -405,6 +405,65 @@ + + +
+ +
+
+ 棚子尺寸 + +
+
+
+ +
+
+
+ + +
+
+ 百叶 + +
+
+
+ + +
+
+
+
+ + +
+ +
+
+ 棚子尺寸 + +
+
+
+ +
+
+
+ + +
+
+ 百叶 + +
+
+
+ +
+
+
+
diff --git a/index.js b/index.js index ca0deba..ae26bc4 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,10 @@ export const initApp = (kernel) => { return kernelInstance; }; + +//全局唯一棚子sku +let pergolaSku = "" + /** * 获取当前 kernel 实例 */ @@ -64,8 +68,6 @@ export const getAutoLoadModelList = async () => { const kernel = getKernel(); const url = apiConfig.getApiUrl('/api/models/auto-load/list') - console.log('API URL:', url) - console.log('apiConfig:', apiConfig) const response = await fetch(url) const data = await response.json() const models = data.data // 这就是模型列表 @@ -74,6 +76,7 @@ export const getAutoLoadModelList = async () => { if (model.placement_zone) { const { alpha, border_color, color, show_border, thickness, walls } = model.placement_zone + kernel.dropZone.setData({ color: color, @@ -98,7 +101,53 @@ export const getAutoLoadModelList = async () => { //获取放置区域 export const getPlacementZone = async (sku) => { const kernel = getKernel(); + console.log(pergolaSku, sku); + let division_include = [] + // 同时包含10和13 + const only10_13 = /(?=.*10)(?=.*13)/.test(pergolaSku) + // 只包含10 无13 无12 + const only10 = /(?=.*10)(?!.*13)(?!.*12)/.test(pergolaSku) + // 同时包含10和12 + const only10_12 = /(?=.*10)(?=.*12)/.test(pergolaSku) + + + // 1. 只要字符串里包含 10,就返回 true + const has10 = /10/.test(sku); + + // 2. 只要字符串里包含 13,就返回 true + const has13 = /13/.test(sku); + + // 2. 只要字符串里包含 12,就返回 true + const has12 = /12/.test(sku); + + + + //包含10,不包含13 + if (only10 && has10) { + console.log('棚子包含10,不包含13 并且配件是10 说明是正方体 或者是10*20的'); + division_include.push('前', '后', '左', '右', "前1", "后1", "前2", "后2") + } + //同时包含10和13 + if (only10_13 && has10) { + console.log('棚子同时包10和13的并且含配件是10'); + division_include.push('左', '右') + } + //同时包含10和13 + if (only10_13 && has13) { + console.log('棚子同时包10和13的并且含配件是13'); + division_include.push('前', '后') + } + if (only10_12 && has12) { + console.log('棚子同时包10和12的并且含配件是12'); + division_include.push('前', '后') + } + if (only10_12 && has10) { + console.log('棚子同时包10和12的并且含配件是12'); + division_include.push('左', '右') + } + + const response = await fetch(apiConfig.getApiUrl(`/api/product-configs/by-sku/${sku}`)); const result = await response.json(); if (result.code === 200) { @@ -107,10 +156,12 @@ export const getPlacementZone = async (sku) => { // const {position_x, position_y, position_z} = data; if (enable_placement_zone && wall_divisions != undefined) { + const filteredDivisions = wall_divisions.filter(item => division_include.includes(item.name)) + console.log(filteredDivisions); // 只清除旧的放置区域网格,不清除模型 kernel.dropZone.clearZones(); - const divisions = wall_divisions.map(wall => ({ - name: wall.name, // 获取最后一个下划线后的部分 + const divisions = filteredDivisions.map(wall => ({ + name: wall.name, divisions: wall.divisions })) @@ -248,12 +299,12 @@ export const executeEvent2 = async (result, sku) => { kernel.model.removeAll(); // 清除所有 SKU 映射 clearAllSkuMappings(); - } + } // 先处理所有 change_model 事件 for (const event of result.data.events) { console.log(event); - + if (event.event_type === 'change_model') { const { target_data } = event; console.log(event.target_data); @@ -359,11 +410,18 @@ export const getProductConfig = async (sku) => { if (result.code === 200) { console.log(result.data); const { enable_placement_zone } = result.data; - // await initPlacementZoneConfig(); + //如果触发的是配件,需要显示放置区域 + if (enable_placement_zone) { + if (pergolaSku === "") { + console.error("请先加载棚子模型") + return; + } getPlacementZone(sku) } + //如果触发的是换棚子模型 else { + pergolaSku = sku; executeEvent2(result, sku) } } diff --git a/package-lock.json b/package-lock.json index 377917a..caafe88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@babylonjs/core": "^9.3.1", "@babylonjs/loaders": "^9.3.1", + "@babylonjs/materials": "^9.8.0", "axios": "^1.6.0", "js-md5": "^0.8.3", "js-yaml": "^4.1.0", @@ -38,6 +39,15 @@ "babylonjs-gltf2interface": "^9.0.0" } }, + "node_modules/@babylonjs/materials": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@babylonjs/materials/-/materials-9.8.0.tgz", + "integrity": "sha512-BqAUtI5QwoN1a/UdcEy1p83rL1vG30uPDnPDRkqtclFl4tWS6O2SEYb770/v2ekgDv4v0BwQvCIgfl2QjRijfw==", + "license": "Apache-2.0", + "peerDependencies": { + "@babylonjs/core": "^9.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", diff --git a/package.json b/package.json index 5166faa..2676cac 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dependencies": { "@babylonjs/core": "^9.3.1", "@babylonjs/loaders": "^9.3.1", + "@babylonjs/materials": "^9.8.0", "axios": "^1.6.0", "js-md5": "^0.8.3", "js-yaml": "^4.1.0", diff --git a/src/babylonjs/AppCamera.ts b/src/babylonjs/AppCamera.ts index 20eb382..6dab45c 100644 --- a/src/babylonjs/AppCamera.ts +++ b/src/babylonjs/AppCamera.ts @@ -31,8 +31,8 @@ export class AppCamera extends Monobehiver { // 限制垂直角范围,实现上帝视角 this.object.upperBetaLimit = Tools.ToRadians(90); // 最大垂直角(接近90度,避免万向锁) - this.object.position = new Vector3(-0, 10, 0); - this.setTarget(0, 2, 0); + this.object.position = new Vector3(0, 10, 0); + this.setTarget(0, 0.5, 0); } /** 设置相机目标点 */ diff --git a/src/babylonjs/AppDropZone.ts b/src/babylonjs/AppDropZone.ts index 0a642b3..d6c00e9 100644 --- a/src/babylonjs/AppDropZone.ts +++ b/src/babylonjs/AppDropZone.ts @@ -123,29 +123,19 @@ export class AppDropZone { '右': ['右', 'right', 'you'] }; - // 匹配墙面名称(支持精确匹配和模糊匹配) + // 匹配墙面名称(精确匹配) const matchWallName = (wallName: string): number | null => { - // 1. 优先精确匹配完整墙面名称 - // 例如:wall.name = "80全铁3x6_前1",divisions 中有 "前1" - for (const [configName, divisionValue] of Object.entries(divisionsMap)) { - if (wallName.includes(configName)) { - return divisionValue; - } - } - - // 2. 如果精确匹配失败,尝试方向模糊匹配(兼容旧版) - const lowerName = wallName.toLowerCase(); - for (const [direction, keywords] of Object.entries(directionKeywords)) { - for (const keyword of keywords) { - if (lowerName.includes(keyword.toLowerCase()) || wallName.includes(keyword)) { - // 检查 divisionsMap 中是否有该方向的配置 - if (divisionsMap[direction] !== undefined) { - return divisionsMap[direction]; - } - } - } + // 提取墙面名称的最后部分(最后一个下划线之后) + // 例如:"SW10000070_10x20星空篷_前1" → "前1" + const wallShortName = wallName.split('_').pop() || wallName; + + // 精确匹配提取出的简短名称 + if (divisionsMap[wallShortName] !== undefined) { + console.log(`墙面 "${wallName}" 通过简短名称 "${wallShortName}" 精确匹配到分割数: ${divisionsMap[wallShortName]}`); + return divisionsMap[wallShortName]; } + console.log(`墙面 "${wallName}" 未匹配到任何分割数配置`); return null; }; diff --git a/src/babylonjs/AppGround.ts b/src/babylonjs/AppGround.ts new file mode 100644 index 0000000..0015f6f --- /dev/null +++ b/src/babylonjs/AppGround.ts @@ -0,0 +1,220 @@ +import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; +import { Mesh } from '@babylonjs/core/Meshes/mesh'; +import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial'; +import { GridMaterial } from '@babylonjs/materials/grid/gridMaterial'; +import { Texture } from '@babylonjs/core/Materials/Textures/texture'; +import { Color3 } from '@babylonjs/core/Maths/math.color'; +import { Vector3 } from '@babylonjs/core/Maths/math.vector'; +import { Monobehiver } from '../base/Monobehiver'; + +/** + * 地面网格配置接口 + */ +export interface GroundConfig { + /** 地面宽度 */ + width?: number; + /** 地面深度 */ + height?: number; + /** 网格细分数 */ + subdivisions?: number; + /** 贴图URL */ + textureUrl?: string; + /** 贴图平铺次数 (uScale, vScale) */ + textureScale?: { u: number; v: number }; + /** 地面颜色 (当没有贴图时使用) */ + color?: Color3; + /** 是否接收阴影 */ + receiveShadows?: boolean; + /** 地面位置 */ + position?: Vector3; + /** 是否显示网格 */ + showGrid?: boolean; + /** 网格线颜色 */ + gridColor?: Color3; + /** 网格大小 */ + gridRatio?: number; +} + +/** + * 地面网格管理类 - 负责创建和管理地面网格 + */ +export class AppGround extends Monobehiver { + ground: Mesh | null; + material: PBRMaterial | null; + gridGround: Mesh | null; + gridMaterial: GridMaterial | null; + private config: GroundConfig; + + constructor(mainApp: any, config: GroundConfig = {}) { + super(mainApp); + this.ground = null; + this.material = null; + this.gridGround = null; + this.gridMaterial = null; + this.config = { + width: 100, + height: 100, + subdivisions: 10, + receiveShadows: true, + position: new Vector3(0, 0, 0), + textureScale: { u: 10, v: 10 }, + color: new Color3(0.5, 0.5, 0.5), + textureUrl: "https://cdn.files.zguiy.com/zt/ground1.jpg", // 默认贴图 + showGrid: true, // 默认显示网格 + gridColor: new Color3(1,1,1), + gridRatio: 1.0, + ...config + }; + } + + /** 初始化地面网格 */ + Awake(): void { + this.createGround(); + this.createMaterial(); + if (this.config.showGrid) { + this.createGridGround(); + } + } + + /** 创建地面网格 */ + private createGround(): void { + if (!this.mainApp.appScene.object) { + console.error('场景未初始化'); + return; + } + + this.ground = MeshBuilder.CreateGround( + 'ground', + { + width: this.config.width, + height: this.config.height, + subdivisions: this.config.subdivisions + }, + this.mainApp.appScene.object + ); + + if (this.config.position) { + this.ground.position = this.config.position; + } + + if (this.config.receiveShadows) { + this.ground.receiveShadows = true; + } + } + + /** 创建材质 */ + private createMaterial(): void { + if (!this.ground || !this.mainApp.appScene.object) return; + + this.material = new PBRMaterial('groundMaterial', this.mainApp.appScene.object); + + // 如果有贴图URL,加载贴图 + if (this.config.textureUrl) { + const texture = new Texture( + this.config.textureUrl, + this.mainApp.appScene.object + ); + + // 设置贴图平铺 + if (this.config.textureScale) { + texture.uScale = this.config.textureScale.u; + texture.vScale = this.config.textureScale.v; + } + + this.material.albedoTexture = texture; + } else { + // 没有贴图时使用纯色 + this.material.albedoColor = this.config.color || new Color3(1,1,1); + } + + // PBR 材质属性设置 + this.material.metallic = 0.0; // 非金属 + this.material.roughness = 1.0; // 粗糙表面 + + this.ground.material = this.material; + } + + /** 创建网格地面 */ + private createGridGround(): void { + if (!this.mainApp.appScene.object) return; + + // 创建网格地面,位置稍微高一点避免z-fighting + this.gridGround = MeshBuilder.CreateGround( + 'gridGround', + { + width: this.config.width, + height: this.config.height, + subdivisions: this.config.subdivisions + }, + this.mainApp.appScene.object + ); + + // 设置位置,稍微高于贴图地面 + const gridPosition = this.config.position ? this.config.position.clone() : new Vector3(0, 0, 0); + gridPosition.y += 0.01; // 抬高0.01单位避免z-fighting + this.gridGround.position = gridPosition; + + // 创建网格材质 + this.gridMaterial = new GridMaterial('gridMaterial', this.mainApp.appScene.object); + this.gridMaterial.mainColor = this.config.gridColor || new Color3(0.3, 0.3, 0.3); + this.gridMaterial.lineColor = this.config.gridColor || new Color3(0.3, 0.3, 0.3); + this.gridMaterial.gridRatio = this.config.gridRatio || 1.0; + this.gridMaterial.opacity = 0.8; + this.gridMaterial.backFaceCulling = false; + + this.gridGround.material = this.gridMaterial; + } + + /** 更新贴图 */ + setTexture(textureUrl: string, uScale: number = 10, vScale: number = 10): void { + if (!this.material || !this.mainApp.appScene.object) return; + + const texture = new Texture(textureUrl, this.mainApp.appScene.object); + texture.uScale = uScale; + texture.vScale = vScale; + this.material.albedoTexture = texture; + } + + /** 更新地面颜色 */ + setColor(color: Color3): void { + if (!this.material) return; + this.material.albedoColor = color; + } + + /** 显示/隐藏地面 */ + setVisible(visible: boolean): void { + if (this.ground) { + this.ground.isVisible = visible; + } + if (this.gridGround) { + this.gridGround.isVisible = visible; + } + } + + /** 显示/隐藏网格 */ + setGridVisible(visible: boolean): void { + if (this.gridGround) { + this.gridGround.isVisible = visible; + } + } + + /** 销毁地面 */ + dispose(): void { + if (this.ground) { + this.ground.dispose(); + this.ground = null; + } + if (this.material) { + this.material.dispose(); + this.material = null; + } + if (this.gridGround) { + this.gridGround.dispose(); + this.gridGround = null; + } + if (this.gridMaterial) { + this.gridMaterial.dispose(); + this.gridMaterial = null; + } + } +} diff --git a/src/babylonjs/MainApp.ts b/src/babylonjs/MainApp.ts index 2d551e5..fade6a1 100644 --- a/src/babylonjs/MainApp.ts +++ b/src/babylonjs/MainApp.ts @@ -19,6 +19,7 @@ import { AppSelectionOutline } from './AppSelectionOutline'; import { AppPositionGizmo } from './AppPositionGizmo'; import { AppModelDrag } from './AppModelDrag'; import { AppDropZone } from './AppDropZone'; +import { AppGround } from './AppGround'; /** * 主应用类 - 3D场景的核心控制器 @@ -38,6 +39,7 @@ export class MainApp { appPositionGizmo: AppPositionGizmo; appModelDrag: AppModelDrag; appDropZone: AppDropZone; + appGround: AppGround; gameManager: GameManager; @@ -54,6 +56,7 @@ export class MainApp { this.appSelectionOutline = new AppSelectionOutline(this); this.appPositionGizmo = new AppPositionGizmo(this); this.appModelDrag = new AppModelDrag(this); + this.appGround = new AppGround(this); this.gameManager = new GameManager(this); window.addEventListener("resize", () => this.appEngin.handleResize()); @@ -76,7 +79,6 @@ export class MainApp { async loadModel(): Promise { await this.appModel.loadModel(); await this.gameManager.Awake(); - console.log(1111111111111111111111); EventBridge.allReady({ scene: this.appScene.object }); } @@ -87,6 +89,7 @@ export class MainApp { this.appCamera.Awake(); this.appLight.Awake(); this.appEnv.Awake(); + this.appGround.Awake(); this.appRay.Awake(); this.appSelectionOutline.init(); this.appPositionGizmo.Awake(); @@ -117,6 +120,7 @@ export class MainApp { this.appEnv?.clean(); this.appPositionGizmo?.dispose(); this.appModelDrag?.dispose(); + this.appGround?.dispose(); // this.appHotspot?.clear(); } }