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