更新打包配置,完善组件入参模式,新增事件总线

This commit is contained in:
陈梓阳 2025-05-08 11:34:35 +08:00
parent cfbfda520d
commit 4a648c2f97
14 changed files with 1091 additions and 26609 deletions

26520
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,13 @@
{
"name": "material-editor",
"private": true,
"version": "1.0.0",
"version": "1.0.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
"preview": "vite preview",
"prepublishOnly": "npm run build"
},
"files": [
"dist",
@ -15,13 +16,15 @@
"index.html",
"README.md"
],
"module": "./dist/material-editor.js",
"main": "./dist/material-editor.umd.js",
"module": "dist/material-editor.es.js",
"main": "dist/material-editor.umd.js",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
"import": "./dist/material-editor.es.js",
"require": "./dist/material-editor.umd.cjs"
},
"./style.css": "./dist/material-editor.css",
"./iife": "./dist/material-editor.iife.js"
},
"dependencies": {
"@jscad/modeling": "^2.11.0",
@ -40,8 +43,10 @@
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3",
"@vue/tsconfig": "^0.7.0",
"sass-embedded": "^1.87.0",
"typescript": "~5.7.2",
"vite": "^6.2.6",
"vite-plugin-dts": "^4.5.3",
"vue-tsc": "^2.2.4"
},
"packageManager": "pnpm@9.1.1+sha1.09ada6cd05003e0ced25fb716f9fda4063ec2e3b"

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import { Viewer } from './Viewer';
import { PMREMGenerator3 } from './PMREMGenerator2';
import type { PhysicalMaterialRecord, TextureTableRecord } from 'webcad_ue4_api';
import { MaterialEditorCameraControl } from './MaterialMouseControl';
import { GetConfig } from '../lib/libOutputConfig';
async function textureRenderUpdate(textureRecord:TextureTableRecord){
const texture = textureRecord['texture'] as Texture;
@ -81,11 +82,6 @@ export class MaterialEditor extends Singleton
this.Viewer.UpdateRender();
this.Viewer.Fov = 90;
}
else
{
this.Canvas.appendChild(document);
}
}
SetViewer(canvas: HTMLElement)
{
@ -119,8 +115,8 @@ export class MaterialEditor extends Singleton
return new Promise(async (res, rej) =>
{
let urls = ['right.webp', 'left.webp', 'top.webp', 'bottom.webp', 'front.webp', 'back.webp'];
new CubeTextureLoader().setPath('./')
let urls = [...GetConfig().envTextureSrc];
new CubeTextureLoader()
.load(urls, (t) =>
{
t.encoding = sRGBEncoding;
@ -148,8 +144,8 @@ export class MaterialEditor extends Singleton
this.exrPromise = new Promise<Texture>((res, rej) =>
{
let urls = ['right-gray.webp', 'left-gray.webp', 'top-gray.webp', 'bottom-gray.webp', 'front-gray.webp', 'back-gray.webp'];
new CubeTextureLoader().setPath('./')
let urls = [...GetConfig().grayEnvTextureSrc];
new CubeTextureLoader()
.load(urls, (t) =>
{
t.encoding = sRGBEncoding;

View File

@ -1,5 +1,21 @@
<template>
<div vertical class="material-adjuster">
<div class="adjust-section">
<h3>操作</h3>
<fieldset v-if="debugMode" style="margin: 1em 0;">
<legend>DEBUG</legend>
<label>上传路径ID</label>
<input v-model="materialInfo.dirId" type="text" placeholder="材质路径ID" />
</fieldset>
<label>材质名</label>
<input v-model.trim="materialInfo.materialName" type="text" placeholder="材质名" />
<!-- <CfFlex gap="1em">
<button class="btn-success" style="min-width: 110px;" @click="HandleUpload">上传</button>
<button class="btn-danger" style="min-width: 110px;" @click="HandleCancel">取消</button>
</CfFlex> -->
</div>
<div class="adjust-section">
<h3>模型预览</h3>
<label>选择模型样式</label>
@ -10,8 +26,8 @@
<div class="adjust-section" v-if="debugMode">
<h3>纹理选择DEBUG</h3>
<label>纹理链接</label>
<input v-model.trim="textureLink" type="text" placeholder="在此键入纹理图像的URL..." />
<button class="btn-primary" @click="scene.ChangeTextureAsync(textureLink)"
<input v-model.trim="_textureSrc" type="text" placeholder="在此键入纹理图像的URL..." />
<button class="btn-primary" @click="scene.ChangeTextureAsync(_textureSrc)"
style="margin-left: 1em;">应用</button>
</div>
<div class="adjust-section">
@ -85,39 +101,41 @@
</div>
</div>
<div class="adjust-section">
<h3>操作</h3>
<fieldset v-if="debugMode" style="margin: 1em 0;">
<legend>DEBUG</legend>
<label>上传路径ID</label>
<input v-model="materialRequest.dirId" type="text" placeholder="材质路径ID" />
</fieldset>
<label>材质名</label>
<input v-model="materialRequest.materialName" type="text" placeholder="材质名" />
<CfFlex gap="1em">
<button class="btn-success" style="min-width: 110px;" @click="HandleUpload">上传</button>
<button class="btn-danger" style="min-width: 110px;" @click="HandleCancel">取消</button>
</CfFlex>
</div>
</div>
</template>
<script setup lang='ts'>
import { ref, reactive, watch } from "vue"
import { ref, reactive, watch, onMounted } from "vue"
import { useScene, type TextureAdjustment } from "../stores/sceneStore";
import { storeToRefs } from "pinia";
import CfFlex from "./CfFlex.vue";
import { DirectoryId } from "../api/Request";
import { IsNullOrWhitespace } from "../helpers/helper.string";
import { DeflateAsync } from "../helpers/helper.compression";
export interface MaterialRequest {
/** 材质名 */
name: string;
/** 材质logo文件 */
logo: Blob;
/** 序列化并Deflate压缩后的材质文件的Base64编码 */
file: string;
}
const scene = useScene();
const debugMode = ref(true);
const props = defineProps<{
textureSrc?: string;
}>();
const emits = defineEmits<{
(e: 'cancel'): void;
(e: 'submit', data: MaterialRequest): void;
}>();
const debugMode = ref(false);
const { CurrGeometry, Geometries, Material } = storeToRefs(scene);
const enableTexture = ref(Material.value.useMap);
const textureLink = ref('');
const _textureSrc = ref(props.textureSrc);
const textureAdjustment = reactive<TextureAdjustment>({
wrapS: 0,
wrapT: 0,
@ -134,9 +152,18 @@ const model = reactive({
normalScale: Material.value.bumpScale,
emissiveIntensity: Material.value.specular
});
const materialRequest = reactive({
const materialInfo = reactive({
dirId: DirectoryId.MaterialDir, // 2
materialName: ''
materialName: '材质1'
});
onMounted(() => {
scene.ChangeTextureAsync(_textureSrc.value);
})
watch(() => props.textureSrc, async (val) => {
_textureSrc.value = val;
await scene.ChangeTextureAsync(_textureSrc.value);
});
watch(model, async (val) => {
@ -171,25 +198,34 @@ async function EnableTexture(enable: boolean) {
async function HandleUpload() {
try {
if (IsNullOrWhitespace(materialRequest.materialName)) {
if (IsNullOrWhitespace(materialInfo.materialName)) {
alert('材质名称不可为空');
return;
}
uploading.value = true;
await scene.UploadMaterialAsync(materialRequest);
const data = {
name: materialInfo.materialName,
logo: await scene.GenerateMaterialLogoAsync(),
// jsonString -> Deflate -> BinaryString -> Base64
file: btoa(String.fromCharCode(...await DeflateAsync(await scene.SerializeMaterialAsync())))
};
emits('submit', data);
return data;
} finally {
uploading.value = false;
alert("上传成功");
} catch (error) {
console.error(error);
alert("上传材质出错,请检查网络连接或联系管理员");
}
}
async function HandleCancel() {
// TODO:
function HandleCancel() {
emits('cancel');
}
defineExpose({
Upload: HandleUpload,
Cancel: HandleCancel
})
</script>
<style scoped>

View File

@ -1,35 +1,59 @@
<template>
<CfFlex class="material-view">
<div ref="container" style="width: 100%; height: 100%; flex: 3; box-sizing: border-box;" />
<MaterialAdjuster style="flex: 1;overflow-y: auto; width: 100%; height: 100%; box-sizing: border-box;" />
<div ref="container" class="material-view-container" />
<MaterialAdjuster ref="adjuster" class="material-view-sider" :texture-src="textureSrc" @cancel="config.cancelCallback" @submit="config.submitCallback" />
</CfFlex>
</template>
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue';
import { onMounted, ref, useTemplateRef } from 'vue';
import MaterialAdjuster from './MaterialAdjuster.vue';
import { useScene } from '../stores/sceneStore';
import CfFlex from './CfFlex.vue';
import { GetConfig } from '../lib/libOutputConfig';
import { useEvent } from '../stores/eventStore';
const scene = useScene();
const container = useTemplateRef<HTMLElement>('container');
const eventbus = useEvent();
const container = useTemplateRef('container');
const adjusterRef = useTemplateRef('adjuster');
const config = GetConfig();
const textureSrc = ref(config.textureSrc);
//
document.addEventListener('contextmenu', (e) => e.preventDefault());
onMounted(() => {
scene.Initial(container.value);
eventbus.Subscribe('submit', () => adjusterRef.value.Upload());
eventbus.Subscribe('update-texture', () => {
textureSrc.value = config.textureSrc
});
});
</script>
<style scoped>
<style scoped lang="scss">
.material-view
{
width: 100%;
height: 100vh;
height: 100%;
box-sizing: border-box;
padding: 0;
margin: 0;
overflow: hidden;
&-container {
flex: 3 1;
height: 100%;
box-sizing: border-box;
overflow: hidden;
}
&-sider {
flex: 1 1;
overflow-y: auto;
height: 100%;
box-sizing: border-box;
}
}
</style>

43
src/lib/entry.ts Normal file
View File

@ -0,0 +1,43 @@
import { createApp, type App as VueApp } from 'vue'
import App from '../App.vue'
import '../assets/main.css'
import { createPinia } from 'pinia';
import { ConfigureLibOutput, type LibOutputConfig } from './libOutputConfig';
import { useEvent } from '../stores/eventStore';
let app: VueApp<Element> = undefined;
/**
*
* @param element HTML元素
* @param options
*/
export function Mount(element: Element, options: Partial<LibOutputConfig>) {
ConfigureLibOutput(options);
const pinia = createPinia();
app = createApp(App);
app.use(pinia)
.mount(element);
}
/** 对程序配置进行更改 */
export function Configure(options: Partial<LibOutputConfig>) {
ConfigureLibOutput(options);
}
/** 卸载已经挂载的DOM */
export function Unmount() {
app.unmount();
}
/** 手动进行材质提交 */
export function Submit() {
useEvent().Publish('submit');
}
/** 更新材质贴图 */
export function UpdateTexture() {
useEvent().Publish('update-texture');
}

View File

@ -1,5 +1,9 @@
import MaterialView from "../components/MaterialView.vue";
import { Mount, Unmount, Submit, UpdateTexture, Configure } from "./entry";
export {
MaterialView
Mount,
Unmount,
Submit,
UpdateTexture,
Configure
}

View File

@ -0,0 +1,38 @@
import type { DeepReadonly } from "vue"
import type { MaterialRequest } from "../components/MaterialAdjuster.vue"
let _libOutputConfig = {
textureSrc: "",
submitCallback: undefined,
cancelCallback: undefined,
envTextureSrc: ['./right.webp', './left.webp', './top.webp', './bottom.webp', './front.webp', './back.webp'],
grayEnvTextureSrc: ['./right-gray.webp', './left-gray.webp', './top-gray.webp', './bottom-gray.webp', './front-gray.webp', './back-gray.webp'],
} as LibOutputConfig
export function ConfigureLibOutput(config: Partial<LibOutputConfig>) {
Object.assign(_libOutputConfig, {
...config
});
// _libOutputConfig = config;
}
export type LibOutputConfig = {
/** 材质贴图链接 */
textureSrc: string,
/** 环境贴图链接(立方体贴图,按照顺序输入[右左上下前后] */
envTextureSrc: string[],
/** 灰度环境贴图链接,输入格式与环境贴图一致 */
grayEnvTextureSrc: string[],
/** 提交材质时的回调 */
submitCallback?: (data: MaterialRequest) => void,
/**
*
* @deprecated 使
*/
cancelCallback?: () => void
}
export function GetConfig(): DeepReadonly<typeof _libOutputConfig>
{
return _libOutputConfig;
}

18
src/stores/eventStore.ts Normal file
View File

@ -0,0 +1,18 @@
import { defineStore } from "pinia";
export const useEvent = defineStore('event', () => {
const events: Partial<Record<EventNames, Function[]>> = {};
function Subscribe(name: EventNames, callback: Function) {
events[name] = events[name] || [];
events[name].push(callback);
}
function Publish(name: EventNames, ...args: any[]) {
events[name]?.forEach(e => e(...args));
}
return { Subscribe, Publish };
});
export type EventNames = 'submit' | 'update-texture'

View File

@ -1,14 +1,11 @@
import { defineStore } from "pinia";
import { computed, ref, watch } from "vue";
import { computed, ref } from "vue";
import { MaterialEditor } from "../common/MaterialEditor";
import { Database, ObjectId, PhysicalMaterialRecord, TextureTableRecord } from "webcad_ue4_api";
import { ObjectId, PhysicalMaterialRecord, TextureTableRecord } from "webcad_ue4_api";
import { LoadImageFromUrl } from "../helpers/helper.imageLoader";
import { Texture } from "three";
import { materialRenderer } from "../common/MaterialRenderer";
import { ImgsUrl, MaterialUrls } from "../api/Api";
import { Post, PostJson, RequestStatus } from "../api/Request";
import { MaterialOut } from "../common/MaterialSerializer";
import { DeflateAsync } from "../helpers/helper.compression";
export const useScene = defineStore('scene', () => {
let _editor: MaterialEditor;
@ -72,7 +69,7 @@ export const useScene = defineStore('scene', () => {
_currTexture.value = record['texture'] as Texture;
_currTexture.value.image = img;
Material.value.map = record.Id;
Material.value.map = img ? record.Id : undefined;
_currTexture.value.needsUpdate = true;
await UpdateMaterialAsync();
}
@ -90,31 +87,28 @@ export const useScene = defineStore('scene', () => {
Update();
}
interface UploadMaterialRequest {
dirId: string;
materialName: string;
}
async function UploadMaterialAsync(request: UploadMaterialRequest) {
async function SerializeMaterialAsync() {
// TODO: Warn: 是否要生成logo路径
// const logoPath = await HandleUpdateLogo();
const matJson = MaterialOut(Material.value as PhysicalMaterialRecord);
console.log(matJson);
return matJson;
}
async function HandleUpdateLogo() {
async function GenerateMaterialLogoAsync() {
const blob = await materialRenderer.getBlob(Material.value.Material);
const file = new File([blob], "blob.png", { type: blob.type });
return blob;
// const file = new File([blob], "blob.png", { type: blob.type });
const formData = new FormData();
formData.append("file", file);
// const formData = new FormData();
// formData.append("file", file);
let data = await Post(ImgsUrl.logo, formData);
// let data = await Post(ImgsUrl.logo, formData);
let logoPath = "";
if (data.err_code === RequestStatus.Ok) {
logoPath = data.images.path;
}
return logoPath;
// let logoPath = "";
// if (data.err_code === RequestStatus.Ok) {
// logoPath = data.images.path;
// }
// return logoPath;
}
return {
@ -126,7 +120,8 @@ export const useScene = defineStore('scene', () => {
UpdateMaterialAsync,
ChangeTextureAsync,
UpdateTexture,
UploadMaterialAsync,
SerializeMaterialAsync,
GenerateMaterialLogoAsync
};
});
@ -139,3 +134,7 @@ export type TextureAdjustment = {
moveX: number,
moveY: number
}
export interface UploadMaterialRequest {
dirId: string;
materialName: string;
}

View File

@ -10,5 +10,5 @@
// "noFallthroughCasesInSwitch": true,
// "noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/lib/index.ts"]
}

10
tsconfig.build.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
},
"include": [
"src"
]
}

View File

@ -1,5 +1,6 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts';
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
@ -7,20 +8,29 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
plugins: [vue(), dts({rollupTypes: true, tsconfigPath: './tsconfig.app.json',insertTypesEntry: true})],
build: {
lib: {
entry: resolve(__dirname, 'src/lib/index.ts'),
name: 'MaterialEditor',
fileName: (format) => `material-editor.${format}.js`
fileName: (format) => `material-editor.${format}.js`,
formats: ['es']
},
rollupOptions: {
external: ['vue'],
// external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
},
// manualChunks: (id) => {
// if (id.includes('node_modules'))
// {
// if(/three/.test(id))
// return 'three';
// }
// return null;
// }
},
}
}
})