diff --git a/packages/chili-core/src/i18n/en.ts b/packages/chili-core/src/i18n/en.ts index 64604f06..4281af06 100644 --- a/packages/chili-core/src/i18n/en.ts +++ b/packages/chili-core/src/i18n/en.ts @@ -39,8 +39,8 @@ export default { "properties.multivalue": "Multi Value", "properties.group.transform": "Transform", "material.texture": "Texture", - "material.width": "Width", - "material.height": "Height", + "material.repeatU": "U repeat", + "material.repeatV": "V repeat", "model.translation": "Translation", "model.rotation": "Rotation", "model.scale": "Scale", diff --git a/packages/chili-core/src/i18n/local.ts b/packages/chili-core/src/i18n/local.ts index 21ed2e73..29059040 100644 --- a/packages/chili-core/src/i18n/local.ts +++ b/packages/chili-core/src/i18n/local.ts @@ -56,8 +56,8 @@ export type I18nKeys = | "properties.multivalue" | "properties.group.transform" | "material.texture" - | "material.width" - | "material.height" + | "material.repeatU" + | "material.repeatV" | "model.translation" | "model.rotation" | "model.scale" diff --git a/packages/chili-core/src/i18n/zh-cn.ts b/packages/chili-core/src/i18n/zh-cn.ts index 44add141..a5f34e47 100644 --- a/packages/chili-core/src/i18n/zh-cn.ts +++ b/packages/chili-core/src/i18n/zh-cn.ts @@ -39,8 +39,8 @@ export default { "properties.multivalue": "多个值", "properties.group.transform": "转换", "material.texture": "贴图", - "material.width": "宽度", - "material.height": "高度", + "material.repeatU": "U 重复", + "material.repeatV": "V 重复", "model.translation": "位移", "model.rotation": "旋转", "model.scale": "缩放", diff --git a/packages/chili-core/src/material.ts b/packages/chili-core/src/material.ts index 5f0cc456..a9a3d8b5 100644 --- a/packages/chili-core/src/material.ts +++ b/packages/chili-core/src/material.ts @@ -50,24 +50,34 @@ export class Material extends HistoryObservable { this.setProperty("texture", value); } - private _width: number = 0; + private _angle: number = 0; @Serializer.serialze() - @Property.define("material.width") - get width(): number { - return this._width; + @Property.define("common.angle") + get angle(): number { + return this._angle; } - set width(value: number) { - this.setProperty("width", value); + set angle(value: number) { + this.setProperty("angle", value); } - private _height: number = 0; + private _repeatU: number = 1; @Serializer.serialze() - @Property.define("material.height") - get height(): number { - return this._height; + @Property.define("material.repeatU") + get repeatU(): number { + return this._repeatU; } - set height(value: number) { - this.setProperty("height", value); + set repeatU(value: number) { + this.setProperty("repeatU", value); + } + + private _repeatV: number = 1; + @Serializer.serialze() + @Property.define("material.repeatV") + get repeatV(): number { + return this._repeatV; + } + set repeatV(value: number) { + this.setProperty("repeatV", value); } constructor(document: IDocument, name: string, color: number, id: string = Id.generate()) { @@ -80,8 +90,9 @@ export class Material extends HistoryObservable { clone(): Material { let material = new Material(this.document, `${this.name} clone`, this.color); material._texture = this._texture; - material._width = this._width; - material._height = this._height; + material._angle = this._angle; + material._repeatU = this._repeatU; + material._repeatV = this._repeatV; return material; } diff --git a/packages/chili-three/src/threeVisualContext.ts b/packages/chili-three/src/threeVisualContext.ts index bf4a3225..0b398c5b 100644 --- a/packages/chili-three/src/threeVisualContext.ts +++ b/packages/chili-three/src/threeVisualContext.ts @@ -13,6 +13,7 @@ import { IVisualShape, LineType, Material, + MathUtils, ShapeMeshData, ShapeType, VertexMeshData, @@ -28,6 +29,7 @@ import { Object3D, Points, PointsMaterial, + RepeatWrapping, Scene, TextureLoader, MeshLambertMaterial as ThreeMaterial, @@ -64,6 +66,8 @@ export class ThreeVisualContext implements IVisualContext { }); if (item.texture) { material.map = new TextureLoader().load(item.texture); + material.map.wrapS = RepeatWrapping; + material.map.wrapT = RepeatWrapping; } item.onPropertyChanged(this.onMaterialPropertyChanged); this.materialMap.set(item.id, material); @@ -89,10 +93,13 @@ export class ThreeVisualContext implements IVisualContext { material.opacity = source.opacity; } else if (prop === "name") { material.name = source.name; - } else if (prop === "width" && material.map) { - material.map.image.width = source.width; - } else if (prop === "height" && material.map) { - material.map.image.height = source.height; + } else if (prop === "angle" && material.map) { + material.map.rotation = MathUtils.degToRad(source.angle); + material.map.center.set(0.5, 0.5); + } else if (prop === "repeatU" && material.map) { + material.map.repeat.setX(source.repeatU); + } else if (prop === "repeatV" && material.map) { + material.map.repeat.setY(source.repeatV); } else { throw new Error("Unknown material property: " + prop); } diff --git a/packages/chili-ui/src/property/material/materialEditor.ts b/packages/chili-ui/src/property/material/materialEditor.ts index e1602619..599c09ae 100644 --- a/packages/chili-ui/src/property/material/materialEditor.ts +++ b/packages/chili-ui/src/property/material/materialEditor.ts @@ -53,7 +53,7 @@ export class MaterialEditor extends HTMLElement { }, }), svg({ - icon: "icon-times", + icon: "icon-trash", onclick: () => { this.dataContent.deleteMaterial(); }, @@ -128,6 +128,10 @@ export class MaterialEditor extends HTMLElement { }; private initEditingControl(material: Material) { + const selectTexture = async () => { + let file = await readFileAsync(".png, .jpg", false, "readAsDataURL"); + material.texture = file.unwrap()[0].data; + }; let container = div({ className: style.properties, }); @@ -146,10 +150,7 @@ export class MaterialEditor extends HTMLElement { backgroundSize: "contain", backgroundImage: new Binding(material, "texture", new UrlStringConverter()), }, - onclick: async () => { - let file = await readFileAsync(".png, .jpeg", false, "readAsDataURL"); - material.texture = file.unwrap()[0].data; - }, + onclick: selectTexture, }), span({ textContent: localize("material.texture"), @@ -166,6 +167,9 @@ export class MaterialEditor extends HTMLElement { Property.getProperties(material).forEach((x) => { appendProperty(container, this.dataContent.document, [material], x); + if (x.display === "material.texture") { + (container.lastChild as HTMLInputElement).onclick = selectTexture; + } }); } } diff --git a/packages/chili/src/document.ts b/packages/chili/src/document.ts index 580f7acf..d94cfe85 100644 --- a/packages/chili/src/document.ts +++ b/packages/chili/src/document.ts @@ -1,11 +1,14 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { + CollectionAction, + CollectionChangedArgs, Constants, History, I18n, IApplication, IDocument, + IHistoryRecord, IModel, INode, INodeLinkedList, @@ -23,6 +26,7 @@ import { Result, Serialized, Serializer, + Transaction, } from "chili-core"; import { Selection } from "./selection"; import { Material } from "chili-core/src/material"; @@ -80,6 +84,7 @@ export class Document extends Observable implements IDocument { this.visual = application.visualFactory.create(this); this.selection = new Selection(this); PubSub.default.sub("nodeLinkedListChanged", this.handleModelChanged); + this.materials.onCollectionChanged(this.handleMaterialChanged); Logger.info(`new document: ${name}`); application.documents.add(this); } @@ -138,7 +143,8 @@ export class Document extends Observable implements IDocument { this.application.views.remove(...views); this.application.activeView = this.application.views.at(0); this.application.documents.delete(this); - + this.materials.removeCollectionChanged(this.handleMaterialChanged); + PubSub.default.remove("nodeLinkedListChanged", this.handleModelChanged); Logger.info(`document: ${this._name} closed`); this.dispose(); } @@ -179,6 +185,32 @@ export class Document extends Observable implements IDocument { return document; } + private handleMaterialChanged = (args: CollectionChangedArgs) => { + if (args.action === CollectionAction.add) { + const record: IHistoryRecord = { + name: "MaterialChanged", + undo: () => { + this.materials.remove(...args.items); + }, + redo: () => { + this.materials.push(...args.items); + }, + }; + Transaction.add(this, record); + } else if (args.action === CollectionAction.remove) { + const record: IHistoryRecord = { + name: "MaterialChanged", + undo: () => { + this.materials.push(...args.items); + }, + redo: () => { + this.materials.remove(...args.items); + }, + }; + Transaction.add(this, record); + } + }; + private handleModelChanged = (document: IDocument, records: NodeRecord[]) => { if (document !== this) return; let adds: INode[] = []; diff --git a/public/iconfont.js b/public/iconfont.js index 58f0075d..1bbe4c18 100644 --- a/public/iconfont.js +++ b/public/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_3585225='',function(v){var h=(h=document.getElementsByTagName("script"))[h.length-1],l=h.getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var a,z,t,i,m,o=function(h,l){l.parentNode.insertBefore(h,l)};if(l&&!v.__iconfont__svg__cssinject__){v.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(h){console&&console.log(h)}}a=function(){var h,l=document.createElement("div");l.innerHTML=v._iconfont_svg_string_3585225,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(h=document.body).firstChild?o(l,h.firstChild):h.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(a,0):(z=function(){document.removeEventListener("DOMContentLoaded",z,!1),a()},document.addEventListener("DOMContentLoaded",z,!1)):document.attachEvent&&(t=a,i=v.document,m=!1,p(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,c())})}function c(){m||(m=!0,t())}function p(){try{i.documentElement.doScroll("left")}catch(h){return void setTimeout(p,50)}c()}}(window); \ No newline at end of file +window._iconfont_svg_string_3585225='',function(v){var h=(h=document.getElementsByTagName("script"))[h.length-1],l=h.getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var a,z,t,i,m,o=function(h,l){l.parentNode.insertBefore(h,l)};if(l&&!v.__iconfont__svg__cssinject__){v.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(h){console&&console.log(h)}}a=function(){var h,l=document.createElement("div");l.innerHTML=v._iconfont_svg_string_3585225,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(h=document.body).firstChild?o(l,h.firstChild):h.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(a,0):(z=function(){document.removeEventListener("DOMContentLoaded",z,!1),a()},document.addEventListener("DOMContentLoaded",z,!1)):document.attachEvent&&(t=a,i=v.document,m=!1,p(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,c())})}function c(){m||(m=!0,t())}function p(){try{i.documentElement.doScroll("left")}catch(h){return void setTimeout(p,50)}c()}}(window); \ No newline at end of file