diff --git a/packages/chili-core/src/application.ts b/packages/chili-core/src/application.ts index 8c92a05b..c0305948 100644 --- a/packages/chili-core/src/application.ts +++ b/packages/chili-core/src/application.ts @@ -20,5 +20,5 @@ export interface IApplication extends IPropertyChanged { activeView: IView | undefined; newDocument(name: string): Promise; openDocument(id: string): Promise; - loadDocument(data: Serialized): Promise; + loadDocument(data: Serialized): Promise; } diff --git a/packages/chili-core/src/config.ts b/packages/chili-core/src/config.ts index eeedd410..c9e558df 100644 --- a/packages/chili-core/src/config.ts +++ b/packages/chili-core/src/config.ts @@ -1,24 +1,25 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { Color, Lazy, PubSub } from "./foundation"; +import { Lazy, PubSub } from "./foundation"; import { ObjectSnapType } from "./snapType"; -export class VisualConfig { - faceEdgeColor: Color = Color.fromRGB(0.75, 0.75, 0.75); - highlightEdgeColor: Color = Color.fromRGB(0.95, 0.95, 0.95); - highlightFaceColor: Color = Color.fromHex(0xfef08a); - selectedEdgeColor: Color = Color.fromRGB(1, 1, 1); - selectedFaceColor: Color = Color.fromHex(0xfde047); - editVertexSize: number = 5; - editVertexColor: Color = Color.fromHex(0x666); - hintVertexSize: number = 3; - hintVertexColor: Color = Color.fromHex(0x666); - trackingVertexSize: number = 5; - trackingVertexColor: Color = Color.fromHex(0x888); - temporaryVertexSize: number = 3; - temporaryVertexColor: Color = Color.fromHex(0x888); - temporaryEdgeColor: Color = Color.fromHex(0x888); -} +export const VisualConfig = { + defaultEdgeColor: 0xcccccc, + defaultFaceColor: 0xdedede, + highlightEdgeColor: 0xfef08a, + highlightFaceColor: 0xfef08a, + selectedEdgeColor: 0xffffff, + selectedFaceColor: 0xfde047, + editVertexSize: 5, + editVertexColor: 0x0000ff, + hintVertexSize: 3, + hintVertexColor: 0x0000ff, + trackingVertexSize: 5, + trackingVertexColor: 0x0000ff, + temporaryVertexSize: 3, + temporaryVertexColor: 0x0000ff, + temporaryEdgeColor: 0xeeeeee, +}; export class Config { private static readonly _lazy = new Lazy(() => new Config()); @@ -27,8 +28,6 @@ export class Config { return this._lazy.value; } - readonly visual: VisualConfig = new VisualConfig(); - private _snapType: ObjectSnapType; get snapType() { return this._snapType; diff --git a/packages/chili-core/src/converter/colorConverter.ts b/packages/chili-core/src/converter/colorConverter.ts index a10c0887..09e3cac5 100644 --- a/packages/chili-core/src/converter/colorConverter.ts +++ b/packages/chili-core/src/converter/colorConverter.ts @@ -1,14 +1,19 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { Color, Result } from "../foundation"; +import { Result } from "../foundation"; import { IConverter } from "./converter"; -export class ColorConverter implements IConverter { - convert(value: Color): Result { - return Result.success(value.toHexStr()); +export class ColorConverter implements IConverter { + convert(value: number): Result { + return Result.success("#" + value.toString(16).padStart(6, "0")); } - convertBack(value: string): Result { - return Color.fromHexStr(value); + convertBack(value: string): Result { + if (value.startsWith("#")) { + value = value.substring(1); + } + let result = parseInt(value, 16); + if (Number.isNaN(value)) return Result.error("Invalid hex string: " + value); + return Result.success(result); } } diff --git a/packages/chili-core/src/document.ts b/packages/chili-core/src/document.ts index 52ae1089..918b987a 100644 --- a/packages/chili-core/src/document.ts +++ b/packages/chili-core/src/document.ts @@ -1,7 +1,8 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { IApplication } from "./application"; -import { History, IDisposable, IPropertyChanged } from "./foundation"; +import { History, IDisposable, IPropertyChanged, ObservableCollection } from "./foundation"; +import { Material } from "./material"; import { INode, INodeLinkedList } from "./model/node"; import { ISelection } from "./selection"; import { ISerialize, Serialized } from "./serialize"; @@ -16,6 +17,7 @@ export interface IDocument extends IPropertyChanged, IDisposable, ISerialize { readonly visual: IVisual; readonly rootNode: INodeLinkedList; readonly application: IApplication; + materials: ObservableCollection; addNode(...nodes: INode[]): void; save(): Promise; close(): Promise; diff --git a/packages/chili-core/src/foundation/color.ts b/packages/chili-core/src/foundation/color.ts deleted file mode 100644 index 6bd3c545..00000000 --- a/packages/chili-core/src/foundation/color.ts +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. - -import { Serializer } from "../serialize"; -import { Result } from "./result"; - -/** - * Color, RGBA format. - * [0, 0, 0, 0] ~ [1, 1, 1, 1 - */ -@Serializer.register("Color", ["r", "g", "b", "a"]) -export class Color { - @Serializer.serialze() - readonly r: number; - - @Serializer.serialze() - readonly g: number; - - @Serializer.serialze() - readonly b: number; - - @Serializer.serialze() - readonly a: number; - - constructor(r: number, g: number, b: number, a: number) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } - - /** - * - * @param hex 0xRRGGBB - * @returns - */ - static fromHex(hex: number): Color { - let r = (hex >> 16) & 0xff; - let g = (hex >> 8) & 0xff; - let b = hex & 0xff; - return new Color(r / 255, g / 255, b / 255, 1); - } - - /** - * - * @param hex #RRGGBB - * @returns - */ - static fromHexStr(hex: string): Result { - if (hex.startsWith("#")) { - hex = hex.substring(1); - } - let value = parseInt(hex, 16); - if (Number.isNaN(value)) return Result.error("Invalid hex string: " + hex); - return Result.success(Color.fromHex(value)); - } - - static fromRGB(r: number, g: number, b: number): Color { - return new Color(r, g, b, 1); - } - - static fromRGBA(r: number, g: number, b: number, a: number): Color { - return new Color(r, g, b, a); - } - - static hue2rgb(p: number, q: number, t: number): number { - if (t < 0) t += 1; - if (t > 1) t -= 1; - if (t < 1 / 6) return p + (q - p) * 6 * t; - if (t < 1 / 2) return q; - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; - return p; - } - - static random(): Color { - return new Color(Math.random(), Math.random(), Math.random(), 1); - } - - static randomAlpha(): Color { - return new Color(Math.random(), Math.random(), Math.random(), Math.random()); - } - - static randomGray(): Color { - let v = Math.random(); - return new Color(v, v, v, 1); - } - - static randomGrayAlpha(): Color { - let v = Math.random(); - return new Color(v, v, v, Math.random()); - } - - static randomRGB(): Color { - return new Color(Math.random(), Math.random(), Math.random(), 1); - } - - static randomRGBA(): Color { - return new Color(Math.random(), Math.random(), Math.random(), Math.random()); - } - - toString(): string { - return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`; - } - - toHexStr(): string { - return "#" + Color.toHex(this.r) + Color.toHex(this.g) + Color.toHex(this.b); - } - - static toHex(n: number): string { - let hex = Math.round(n * 255).toString(16); - if (hex.length < 2) { - hex = "0" + hex; - } - return hex; - } -} - -export class Colors { - static readonly Red = new Color(1, 0, 0, 1); - static readonly Green = new Color(0, 1, 0, 1); - static readonly Blue = new Color(0, 0, 1, 1); - static readonly White = new Color(1, 1, 1, 1); - static readonly Black = new Color(0, 0, 0, 1); - static readonly Yellow = new Color(1, 1, 0, 1); - static readonly Cyan = new Color(0, 1, 1, 1); - static readonly Magenta = new Color(1, 0, 1, 1); - static readonly Gray = new Color(0.5, 0.5, 0.5, 1); - static readonly LightGray = new Color(0.8, 0.8, 0.8, 1); - static readonly DarkGray = new Color(0.2, 0.2, 0.2, 1); - static readonly Transparent = new Color(0, 0, 0, 0); - static readonly Orange = new Color(1, 0.5, 0, 1); - static readonly Brown = new Color(0.6, 0.4, 0.2, 1); - static readonly Pink = new Color(1, 0.6, 0.6, 1); - static readonly Purple = new Color(0.6, 0.2, 0.8, 1); - static readonly Lime = new Color(0.6, 1, 0.2, 1); - static readonly Teal = new Color(0.2, 0.6, 0.6, 1); - static readonly Sky = new Color(0.2, 0.6, 1, 1); - static readonly Indigo = new Color(0.2, 0.2, 0.6, 1); - static readonly Olive = new Color(0.6, 0.6, 0.2, 1); - static readonly Maroon = new Color(0.6, 0.2, 0.2, 1); -} diff --git a/packages/chili-core/src/foundation/index.ts b/packages/chili-core/src/foundation/index.ts index 818a262f..1c2a3209 100644 --- a/packages/chili-core/src/foundation/index.ts +++ b/packages/chili-core/src/foundation/index.ts @@ -2,7 +2,6 @@ export * from "./asyncController"; export * from "./collection"; -export * from "./color"; export * from "./disposable"; export * from "./dto"; export * from "./equalityComparer"; diff --git a/packages/chili-core/src/foundation/pubsub.ts b/packages/chili-core/src/foundation/pubsub.ts index ac828ce1..cf96cb80 100644 --- a/packages/chili-core/src/foundation/pubsub.ts +++ b/packages/chili-core/src/foundation/pubsub.ts @@ -3,6 +3,7 @@ import { CommandKeys, ICommand } from "../command"; import { IDocument } from "../document"; import { I18nKeys } from "../i18n"; +import { Material } from "../material"; import { IModel, INode } from "../model"; import { ObjectSnapType } from "../snapType"; import { CursorType, IView } from "../visual"; @@ -39,6 +40,7 @@ export interface PubSubEventMap { showPermanent: (action: () => Promise, message: I18nKeys, ...args: any[]) => void; showDialog: (title: I18nKeys, context: IPropertyChanged, callback: () => void) => void; viewCursor: (cursor: CursorType) => void; + editMaterial: (document: IDocument, material: Material, callback: (material: Material) => void) => void; } export class PubSub implements IDisposable { diff --git a/packages/chili-core/src/foundation/utils/readFileAsync.ts b/packages/chili-core/src/foundation/utils/readFileAsync.ts index 6d7248b9..bdc96992 100644 --- a/packages/chili-core/src/foundation/utils/readFileAsync.ts +++ b/packages/chili-core/src/foundation/utils/readFileAsync.ts @@ -7,7 +7,11 @@ export interface FileData { data: string; } -export async function readFileAsync(accept: string, multiple: boolean): Promise> { +export async function readFileAsync( + accept: string, + multiple: boolean, + reader: "readAsText" | "readAsDataURL" = "readAsText", +): Promise> { return new Promise((resolve, _reject) => { let result: FileData[] = []; let input = document.createElement("input"); @@ -17,7 +21,7 @@ export async function readFileAsync(accept: string, multiple: boolean): Promise< input.accept = accept; input.onchange = async () => { document.body.removeChild(input); - await resolveFiles(input, result, resolve); + await resolveFiles(input, result, resolve, reader); }; input.oncancel = () => { document.body.removeChild(input); @@ -32,6 +36,7 @@ async function resolveFiles( input: HTMLInputElement, result: FileData[], resolve: (value: Result | PromiseLike>) => void, + reader: "readAsText" | "readAsDataURL", ) { if (!input.files) { resolve(Result.error(`no files`)); @@ -40,7 +45,7 @@ async function resolveFiles( for (let i = 0; i < input.files.length; i++) { let file = input.files.item(i); if (!file) continue; - let data = await asyncFileReader(file); + let data = await asyncFileReader(file, reader); if (data.success) { result.push({ fileName: file.name, @@ -53,7 +58,7 @@ async function resolveFiles( resolve(Result.success(result)); } -function asyncFileReader(file: File): Promise> { +function asyncFileReader(file: File, method: any): Promise> { return new Promise((resolve, reject) => { let reader = new FileReader(); reader.onload = (e) => { @@ -62,6 +67,6 @@ function asyncFileReader(file: File): Promise> { reader.onerror = (e) => { resolve(Result.error(`Error occurred reading file: ${file.name}`)); }; - reader.readAsText(file); + (reader as any)[method](file); }); } diff --git a/packages/chili-core/src/geometry/meshData.ts b/packages/chili-core/src/geometry/meshData.ts index 5da7a35f..750fb335 100644 --- a/packages/chili-core/src/geometry/meshData.ts +++ b/packages/chili-core/src/geometry/meshData.ts @@ -1,7 +1,6 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { Config } from "../config"; -import { Color, Colors } from "../foundation"; +import { VisualConfig } from "../config"; import { XYZ } from "../math"; import { LineType } from "./lineType"; import { IShape } from "./shape"; @@ -22,7 +21,7 @@ export interface ShapeMeshGroup { export interface ShapeMeshData { positions: number[]; groups: ShapeMeshGroup[]; - color: Color | number[]; + color: number | number[]; } export namespace ShapeMeshData { @@ -44,7 +43,7 @@ export interface VertexMeshData extends ShapeMeshData { } export namespace VertexMeshData { - export function from(point: XYZ, size: number, color: Color): VertexMeshData { + export function from(point: XYZ, size: number, color: number): VertexMeshData { return { positions: [point.x, point.y, point.z], groups: [], @@ -59,7 +58,7 @@ export interface EdgeMeshData extends ShapeMeshData { } export namespace EdgeMeshData { - export function from(start: XYZ, end: XYZ, color: Color, lineType: LineType): EdgeMeshData { + export function from(start: XYZ, end: XYZ, color: number, lineType: LineType): EdgeMeshData { return { positions: [start.x, start.y, start.z, end.x, end.y, end.z], color, @@ -78,10 +77,10 @@ export interface FaceMeshData extends ShapeMeshData { export abstract class MeshDataBuilder { protected readonly _positions: number[] = []; protected readonly _groups: ShapeMeshGroup[] = []; - protected _color: Color | undefined; + protected _color: number | undefined; protected _vertexColor: number[] | undefined; - setColor(color: Color) { + setColor(color: number) { this._color = color; } @@ -92,7 +91,7 @@ export abstract class MeshDataBuilder { } protected getColor() { - let color: Color | number[] | undefined = this._vertexColor; + let color: number | number[] | undefined = this._vertexColor; if (this._vertexColor?.length !== this._positions.length) { color = this._color; } @@ -118,7 +117,7 @@ export class EdgeMeshDataBuilder extends MeshDataBuilder { constructor() { super(); - this._color = Config.instance.visual.faceEdgeColor; + this._color = VisualConfig.defaultEdgeColor; } setType(type: LineType) { @@ -168,7 +167,7 @@ export class FaceMeshDataBuilder extends MeshDataBuilder { constructor() { super(); - this._color = Colors.Gray; + this._color = VisualConfig.defaultFaceColor; } override newGroup() { diff --git a/packages/chili-core/src/i18n/en.ts b/packages/chili-core/src/i18n/en.ts index 6b5be215..64604f06 100644 --- a/packages/chili-core/src/i18n/en.ts +++ b/packages/chili-core/src/i18n/en.ts @@ -17,6 +17,7 @@ export default { "common.length": "Length", "common.angle": "Angle", "common.back": "Back", + "common.material": "Material", "home.welcome": "Welcome to chili3d", "home.recent": "Recent", "ribbon.tab.file": "File", @@ -37,6 +38,9 @@ export default { "properties.header": "Properties", "properties.multivalue": "Multi Value", "properties.group.transform": "Transform", + "material.texture": "Texture", + "material.width": "Width", + "material.height": "Height", "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 1b5b265f..21ed2e73 100644 --- a/packages/chili-core/src/i18n/local.ts +++ b/packages/chili-core/src/i18n/local.ts @@ -20,6 +20,7 @@ export type I18nKeys = | "common.angle" | "common.type" | "common.back" + | "common.material" | "home.welcome" | "home.recent" | "body.arc" @@ -54,6 +55,9 @@ export type I18nKeys = | "properties.header" | "properties.multivalue" | "properties.group.transform" + | "material.texture" + | "material.width" + | "material.height" | "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 33815007..44add141 100644 --- a/packages/chili-core/src/i18n/zh-cn.ts +++ b/packages/chili-core/src/i18n/zh-cn.ts @@ -17,6 +17,7 @@ export default { "common.angle": "角度", "common.type": "类型", "common.back": "返回", + "common.material": "材质", "home.welcome": "欢迎使用 chili3d", "home.recent": "最近使用", "ribbon.tab.file": "文件", @@ -37,6 +38,9 @@ export default { "properties.header": "属性", "properties.multivalue": "多个值", "properties.group.transform": "转换", + "material.texture": "贴图", + "material.width": "宽度", + "material.height": "高度", "model.translation": "位移", "model.rotation": "旋转", "model.scale": "缩放", diff --git a/packages/chili-core/src/index.ts b/packages/chili-core/src/index.ts index fb6dc4b8..362617b7 100644 --- a/packages/chili-core/src/index.ts +++ b/packages/chili-core/src/index.ts @@ -10,6 +10,7 @@ export * from "./editor"; export * from "./foundation"; export * from "./geometry"; export * from "./i18n"; +export * from "./material"; export * from "./math"; export * from "./model"; export * from "./property"; diff --git a/packages/chili-core/src/material.ts b/packages/chili-core/src/material.ts new file mode 100644 index 00000000..5f0cc456 --- /dev/null +++ b/packages/chili-core/src/material.ts @@ -0,0 +1,88 @@ +// Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. + +import { IDocument } from "./document"; +import { HistoryObservable, Id } from "./foundation"; +import { Property } from "./property"; +import { Serializer } from "./serialize"; + +@Serializer.register("Material", ["document", "name", "color", "id"]) +export class Material extends HistoryObservable { + @Serializer.serialze() + readonly id: string; + + private _name: string; + @Serializer.serialze() + @Property.define("common.name") + get name(): string { + return this._name; + } + set name(value: string) { + this.setProperty("name", value); + } + + private _color: number; + @Serializer.serialze() + @Property.define("common.color", undefined, undefined, "color") + get color(): number { + return this._color; + } + set color(value: number) { + this.setProperty("color", value); + } + + private _opacity: number = 1; + @Serializer.serialze() + @Property.define("common.opacity") + get opacity(): number { + return this._opacity; + } + set opacity(value: number) { + this.setProperty("opacity", value); + } + + private _texture: string = ""; + @Serializer.serialze() + @Property.define("material.texture") + get texture() { + return this._texture; + } + set texture(value: string) { + this.setProperty("texture", value); + } + + private _width: number = 0; + @Serializer.serialze() + @Property.define("material.width") + get width(): number { + return this._width; + } + set width(value: number) { + this.setProperty("width", value); + } + + private _height: number = 0; + @Serializer.serialze() + @Property.define("material.height") + get height(): number { + return this._height; + } + set height(value: number) { + this.setProperty("height", value); + } + + constructor(document: IDocument, name: string, color: number, id: string = Id.generate()) { + super(document); + this.id = id; + this._name = name; + this._color = color; + } + + 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; + + return material; + } +} diff --git a/packages/chili-core/src/model/model.ts b/packages/chili-core/src/model/model.ts index 925ea79f..dc1658db 100644 --- a/packages/chili-core/src/model/model.ts +++ b/packages/chili-core/src/model/model.ts @@ -1,7 +1,7 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { IDocument } from "../document"; -import { Color, Colors, Logger } from "../foundation"; +import { Logger } from "../foundation"; import { Id } from "../foundation/id"; import { ICompound, IShape } from "../geometry"; import { Matrix4 } from "../math"; @@ -44,31 +44,27 @@ export abstract class Model extends Node implements I ); } - private _color: Color = Colors.Gray; - + private _materialId: string; @Serializer.serialze() - @Property.define("common.color") - get color() { - return this._color; - } - set color(value: Color) { - this.setProperty("color", value); + @Property.define("common.material") + get materialId(): string { + return this._materialId; } - private _opacity: number = 1; - - @Serializer.serialze() - @Property.define("common.opacity") - get opacity() { - return this._opacity; - } - set opacity(value: number) { - this.setProperty("opacity", value); + set materialId(value: string) { + this.setProperty("materialId", value); } - constructor(document: IDocument, name: string, body: Body, id: string = Id.generate()) { + constructor( + document: IDocument, + name: string, + body: Body, + materialId: string, + id: string = Id.generate(), + ) { super(document, name, id); this.body = body; + this._materialId = materialId; } } @@ -82,7 +78,7 @@ export class GeometryModel extends Model { } constructor(document: IDocument, name: string, body: Body, id: string = Id.generate()) { - super(document, name, body, id); + super(document, name, body, document.materials.at(0)!.id, id); this.drawShape(); body.onShapeChanged(this.onShapeChanged); } diff --git a/packages/chili-core/src/model/node.ts b/packages/chili-core/src/model/node.ts index 7575b12f..28099729 100644 --- a/packages/chili-core/src/model/node.ts +++ b/packages/chili-core/src/model/node.ts @@ -1,7 +1,7 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { IDocument } from "../document"; -import { Color, HistoryObservable, IDisposable, IPropertyChanged } from "../foundation"; +import { HistoryObservable, IDisposable, IPropertyChanged } from "../foundation"; import { Id } from "../foundation/id"; import { IShape } from "../geometry"; import { Matrix4 } from "../math"; @@ -34,9 +34,8 @@ export interface INodeLinkedList extends INode { export interface IModel extends INode { readonly document: IDocument; readonly body: Entity; + materialId: string; matrix: Matrix4; - color: Color; - opacity: number; shape(): IShape | undefined; } diff --git a/packages/chili-core/src/property.ts b/packages/chili-core/src/property.ts index 016144cc..6871a55b 100644 --- a/packages/chili-core/src/property.ts +++ b/packages/chili-core/src/property.ts @@ -3,12 +3,15 @@ import { IConverter } from "./converter"; import { I18nKeys } from "./i18n"; +export type PropertyType = "color"; + export interface Property { name: string; display: I18nKeys; converter?: IConverter; group?: I18nKeys; icon?: string; + type?: PropertyType; dependencies?: { property: string | number | symbol; value: any; @@ -18,9 +21,9 @@ export interface Property { const PropertyKeyMap = new Map>(); export namespace Property { - export function define(display: I18nKeys, group?: I18nKeys, icon?: string) { + export function define(display: I18nKeys, group?: I18nKeys, icon?: string, type?: PropertyType) { return (target: Object, name: string) => { - setProperty(target, name, { display, group, icon }); + setProperty(target, name, { display, group, icon, type }); }; } diff --git a/packages/chili-core/src/serialize/serializer.ts b/packages/chili-core/src/serialize/serializer.ts index fb072658..fd7e9441 100644 --- a/packages/chili-core/src/serialize/serializer.ts +++ b/packages/chili-core/src/serialize/serializer.ts @@ -105,16 +105,20 @@ export namespace Serializer { parameters["document"] = document; for (const key of ctorParamNames) { if (key in properties) { - parameters[key] = deserialValue(document, properties[key]); + if (properties[key] !== undefined) + parameters[key] = deserialValue(document, properties[key]); } else if (key !== "document") { - throw new Error(`${className} constructor parameter ${key} is missing`); + parameters[key] = undefined; + console.warn(`${className} constructor parameter ${key} is missing`); } } return parameters; } function deserialValue(document: IDocument, value: any) { - if (Array.isArray(value)) { + if (value === undefined) { + return undefined; + } else if (Array.isArray(value)) { return value.map((v) => { return typeof v === "object" ? deserializeObject(document, v) : v; }); diff --git a/packages/chili-core/src/visual/textGenerator.ts b/packages/chili-core/src/visual/textGenerator.ts index 568b6d45..065dd616 100644 --- a/packages/chili-core/src/visual/textGenerator.ts +++ b/packages/chili-core/src/visual/textGenerator.ts @@ -1,8 +1,7 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { Color } from "../foundation"; import { IVisualObject } from "./visualObject"; export interface ITextGenerator { - generate(text: string, size: number, color: Color, font: "fzhei"): Promise; + generate(text: string, size: number, color: number, font: "fzhei"): Promise; } diff --git a/packages/chili-core/src/visual/visualShape.ts b/packages/chili-core/src/visual/visualShape.ts index afec2214..7c960c05 100644 --- a/packages/chili-core/src/visual/visualShape.ts +++ b/packages/chili-core/src/visual/visualShape.ts @@ -1,6 +1,5 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { Color } from "../foundation"; import { IShape, ShapeType } from "../geometry"; import { IVisualObject } from "./visualObject"; @@ -32,8 +31,6 @@ export interface VisualGroup { export interface IVisualShape extends IVisualObject { get shape(): IShape; - color: Color; - opacity: number; addState(state: VisualState, type: ShapeType, ...indexes: number[]): void; removeState(state: VisualState, type: ShapeType, ...indexes: number[]): void; resetState(): void; diff --git a/packages/chili-occ/src/occMesh.ts b/packages/chili-occ/src/occMesh.ts index 3b09fcd1..105b16f0 100644 --- a/packages/chili-occ/src/occMesh.ts +++ b/packages/chili-occ/src/occMesh.ts @@ -155,6 +155,7 @@ export class OccMesh implements IShapeMeshData { let poly = handlePoly.get(); let trsf = location.Transformation(); this.addNodes(poly, trsf, builder); + this.addUVs(poly, builder); this.addTriangles(poly, face.Orientation_1(), builder); this.addNormals(handlePoly, trsf, face, poly.NbNodes(), builder); builder.endGroup(OccHelps.wrapShape(face)); @@ -176,6 +177,22 @@ export class OccMesh implements IShapeMeshData { } } + private addUVs(poly: Poly_Triangulation, builder: FaceMeshDataBuilder) { + let us = [], + vs = []; + for (let index = 1; index <= poly.NbNodes(); index++) { + us.push(poly.UVNode(index).X()); + vs.push(poly.UVNode(index).Y()); + } + let minU = Math.min(...us), + maxU = Math.max(...us); + let minV = Math.min(...vs), + maxV = Math.max(...vs); + for (let index = 0; index < us.length; index++) { + builder.addUV((us[index] - minU) / (maxU - minU), (vs[index] - minV) / (maxV - minV)); + } + } + private addNodes(poly: Poly_Triangulation, transform: gp_Trsf, builder: MeshDataBuilder) { for (let index = 1; index <= poly.NbNodes(); index++) { const pnt = poly.Node(index).Transformed(transform); diff --git a/packages/chili-three/src/threeHelper.ts b/packages/chili-three/src/threeHelper.ts index 7d37776b..529e26eb 100644 --- a/packages/chili-three/src/threeHelper.ts +++ b/packages/chili-three/src/threeHelper.ts @@ -1,6 +1,6 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { Color, Matrix4, XYZ } from "chili-core"; +import { Matrix4, XYZ } from "chili-core"; import { Box3, Camera, @@ -32,12 +32,12 @@ export class ThreeHelper { return (camera as OrthographicCamera).isOrthographicCamera; } - static fromColor(color: Color): ThreeColor { - return new ThreeColor(color.r, color.g, color.b); + static fromColor(color: number): ThreeColor { + return new ThreeColor(color); } - static toColor(color: ThreeColor): Color { - return new Color(color.r, color.g, color.b, 1); + static toColor(color: ThreeColor): number { + return color.getHex(); } static findGroupIndex(groups: { start: number; count: number }[], subIndex: number) { diff --git a/packages/chili-three/src/threeShape.ts b/packages/chili-three/src/threeShape.ts index 8cf621b1..1efc76ee 100644 --- a/packages/chili-three/src/threeShape.ts +++ b/packages/chili-three/src/threeShape.ts @@ -1,8 +1,6 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { - Color, - Config, EdgeMeshData, FaceMeshData, IHighlighter, @@ -11,6 +9,7 @@ import { Matrix4, ShapeMeshData, ShapeType, + VisualConfig, VisualState, } from "chili-core"; import { @@ -21,7 +20,7 @@ import { LineSegments, Material, Mesh, - MeshStandardMaterial, + MeshLambertMaterial, Object3D, Color as ThreeColor, } from "three"; @@ -29,21 +28,21 @@ import { import { ThreeHelper } from "./threeHelper"; const hilightEdgeMaterial = new LineBasicMaterial({ - color: ThreeHelper.fromColor(Config.instance.visual.highlightEdgeColor), + color: ThreeHelper.fromColor(VisualConfig.highlightEdgeColor), polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1, }); const selectedEdgeMaterial = new LineBasicMaterial({ - color: ThreeHelper.fromColor(Config.instance.visual.selectedEdgeColor), + color: ThreeHelper.fromColor(VisualConfig.selectedEdgeColor), polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1, }); -const highlightFaceMaterial = new MeshStandardMaterial({ - color: ThreeHelper.fromColor(Config.instance.visual.highlightFaceColor), +const highlightFaceMaterial = new MeshLambertMaterial({ + color: ThreeHelper.fromColor(VisualConfig.highlightFaceColor), side: DoubleSide, transparent: true, opacity: 0.85, @@ -52,8 +51,8 @@ const highlightFaceMaterial = new MeshStandardMaterial({ polygonOffsetUnits: -1, }); -const selectedFaceMaterial = new MeshStandardMaterial({ - color: ThreeHelper.fromColor(Config.instance.visual.selectedFaceColor), +const selectedFaceMaterial = new MeshLambertMaterial({ + color: ThreeHelper.fromColor(VisualConfig.selectedFaceColor), side: DoubleSide, transparent: true, opacity: 0.32, @@ -66,10 +65,7 @@ export class ThreeShape extends Object3D implements IVisualShape { private readonly _highlightedFaces: Map = new Map(); private readonly _highlightedEdges: Map = new Map(); - private _faceMaterial = new MeshStandardMaterial({ - side: DoubleSide, - transparent: true, - }); + private _faceMaterial: Material; private _edgeMaterial = new LineBasicMaterial(); private _edges?: LineSegments; private _faces?: Mesh; @@ -79,20 +75,11 @@ export class ThreeShape extends Object3D implements IVisualShape { return this._edgeMaterial; } - set color(color: Color) { - this.getMainMaterial().color = ThreeHelper.fromColor(color); - } - - get color(): Color { - return ThreeHelper.toColor(this.getMainMaterial().color); - } - - get opacity() { - return this.getMainMaterial().opacity; - } - - set opacity(value: number) { - this.getMainMaterial().opacity = value; + setFaceMaterial(material: Material) { + if (this._faces) { + this._faceMaterial = material; + this._faces.material = material; + } } get transform() { @@ -106,9 +93,11 @@ export class ThreeShape extends Object3D implements IVisualShape { constructor( readonly shape: IShape, readonly highlighter: IHighlighter, + material: Material, ) { super(); let mesh = this.shape.mesh; + this._faceMaterial = material; this.matrixAutoUpdate = false; if (mesh.faces?.positions.length) this.add(this.initFaces(mesh.faces)); if (mesh.edges?.positions.length) this.add(this.initEdges(mesh.edges)); @@ -122,7 +111,6 @@ export class ThreeShape extends Object3D implements IVisualShape { this._faces.geometry.dispose(); } this._edgeMaterial.dispose(); - this._faceMaterial.dispose(); this.resetState(); } @@ -140,8 +128,9 @@ export class ThreeShape extends Object3D implements IVisualShape { let buff = new BufferGeometry(); buff.setAttribute("position", new Float32BufferAttribute(data.positions, 3)); buff.setAttribute("normal", new Float32BufferAttribute(data.normals, 3)); + buff.setAttribute("uv", new Float32BufferAttribute(data.uvs, 2)); buff.setIndex(data.indices); - this.initColor(data, buff, this._faceMaterial); + // this.initColor(data, buff, this._faceMaterial); buff.computeBoundingBox(); this._faces = new Mesh(buff, this._faceMaterial); return this._faces; @@ -150,13 +139,13 @@ export class ThreeShape extends Object3D implements IVisualShape { private initColor( meshData: ShapeMeshData, geometry: BufferGeometry, - material: LineBasicMaterial | MeshStandardMaterial, + material: LineBasicMaterial | MeshLambertMaterial, ) { if (meshData.color instanceof Array) { material.vertexColors = true; geometry.setAttribute("color", new Float32BufferAttribute(meshData.color, 3)); } else { - material.color = new ThreeColor(meshData.color.toHexStr()); + material.color = new ThreeColor(meshData.color); } } @@ -176,7 +165,7 @@ export class ThreeShape extends Object3D implements IVisualShape { ) { if (type === ShapeType.Shape) { let newState = this.highlighter.updateStateData(this, action, state, type); - this.setMaterial(newState); + this.setStateMaterial(newState); } else { indexes.forEach((index) => { let newState = this.highlighter.updateStateData(this, action, state, type, index); @@ -195,7 +184,7 @@ export class ThreeShape extends Object3D implements IVisualShape { // TODO: other type } - private setMaterial(newState: VisualState) { + private setStateMaterial(newState: VisualState) { if (this._faces) { let faceMaterial = this._faceMaterial; if (VisualState.hasState(newState, VisualState.selected)) { @@ -297,7 +286,7 @@ export class ThreeShape extends Object3D implements IVisualShape { } } - private cloneSubFace(index: number, material: MeshStandardMaterial) { + private cloneSubFace(index: number, material: MeshLambertMaterial) { let group = this.shape.mesh.faces!.groups[index]; if (!group) return undefined; let allPositions = this._faces!.geometry.getAttribute("position") as Float32BufferAttribute; diff --git a/packages/chili-three/src/threeTextGenerator.ts b/packages/chili-three/src/threeTextGenerator.ts index 3ae81949..df71b554 100644 --- a/packages/chili-three/src/threeTextGenerator.ts +++ b/packages/chili-three/src/threeTextGenerator.ts @@ -1,20 +1,19 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { Color, ITextGenerator } from "chili-core"; +import { ITextGenerator } from "chili-core"; import { DoubleSide, MeshBasicMaterial, ShapeGeometry } from "three"; import { Font, FontLoader } from "three/examples/jsm/loaders/FontLoader.js"; -import { ThreeHelper } from "./threeHelper"; import { ThreeMeshObject } from "./threeMeshObject"; export class ThreeTextGenerator implements ITextGenerator { private readonly _fonts: Map = new Map(); - async generate(text: string, size: number, color: Color, fontName = "fzhei") { + async generate(text: string, size: number, color: number, fontName = "fzhei") { let font = await this._getFont(fontName); let shapes = font.generateShapes(text, size); const geometry = new ShapeGeometry(shapes); let material = new MeshBasicMaterial({ - color: ThreeHelper.fromColor(color), + color: color, side: DoubleSide, }); return new ThreeMeshObject(geometry, material); diff --git a/packages/chili-three/src/threeVisualContext.ts b/packages/chili-three/src/threeVisualContext.ts index 88bf7402..bf4a3225 100644 --- a/packages/chili-three/src/threeVisualContext.ts +++ b/packages/chili-three/src/threeVisualContext.ts @@ -1,22 +1,25 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { - Color, + CollectionAction, + CollectionChangedArgs, EdgeMeshData, IDisposable, IModel, INode, IVisual, IVisualContext, + IVisualObject, IVisualShape, LineType, + Material, ShapeMeshData, ShapeType, VertexMeshData, } from "chili-core"; -import { IVisualObject } from "chili-core/src/visual/visualObject"; import { BufferGeometry, + DoubleSide, Float32BufferAttribute, Group, LineBasicMaterial, @@ -26,14 +29,16 @@ import { Points, PointsMaterial, Scene, + TextureLoader, + MeshLambertMaterial as ThreeMaterial, } from "three"; -import { ThreeHelper } from "./threeHelper"; import { ThreeShape } from "./threeShape"; import { ThreeVisualObject } from "./threeVisualObject"; export class ThreeVisualContext implements IVisualContext { private readonly _shapeModelMap = new WeakMap(); private readonly _modelShapeMap = new WeakMap(); + private readonly materialMap = new Map(); readonly visualShapes: Group; readonly tempShapes: Group; @@ -45,8 +50,54 @@ export class ThreeVisualContext implements IVisualContext { this.visualShapes = new Group(); this.tempShapes = new Group(); scene.add(this.visualShapes, this.tempShapes); + visual.document.materials.onCollectionChanged(this.onMaterialsChanged); } + private onMaterialsChanged = (args: CollectionChangedArgs) => { + if (args.action === CollectionAction.add) { + args.items.forEach((item: Material) => { + let material = new ThreeMaterial({ + color: item.color, + side: DoubleSide, + transparent: true, + name: item.name, + }); + if (item.texture) { + material.map = new TextureLoader().load(item.texture); + } + item.onPropertyChanged(this.onMaterialPropertyChanged); + this.materialMap.set(item.id, material); + }); + } else if (args.action === CollectionAction.remove) { + args.items.forEach((item: Material) => { + let material = this.materialMap.get(item.id); + this.materialMap.delete(item.id); + item.removePropertyChanged(this.onMaterialPropertyChanged); + material?.dispose(); + }); + } + }; + + private onMaterialPropertyChanged = (prop: keyof Material, source: Material) => { + let material = this.materialMap.get(source.id); + if (!material) return; + if (prop === "color") { + material.color.set(source.color); + } else if (prop === "texture") { + material.map = source.texture ? new TextureLoader().load(source.texture) : null; + } else if (prop === "opacity") { + 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 { + throw new Error("Unknown material property: " + prop); + } + }; + addMesh(data: ShapeMeshData): IVisualObject { let shape: ThreeVisualObject | undefined = undefined; if (ShapeMeshData.isVertex(data)) { @@ -80,6 +131,12 @@ export class ThreeVisualContext implements IVisualContext { } if (IDisposable.isDisposable(x)) x.dispose(); }); + this.visual.document.materials.forEach((x) => + x.removePropertyChanged(this.onMaterialPropertyChanged), + ); + this.visual.document.materials.removeCollectionChanged(this.onMaterialsChanged); + this.materialMap.forEach((x) => x.dispose()); + this.materialMap.clear(); this.visualShapes.clear(); this.tempShapes.clear(); this.scene.remove(this.visualShapes, this.tempShapes); @@ -133,7 +190,7 @@ export class ThreeVisualContext implements IVisualContext { private createEdgeGeometry(data: EdgeMeshData) { let buff = new BufferGeometry(); buff.setAttribute("position", new Float32BufferAttribute(data.positions, 3)); - let color = ThreeHelper.fromColor(data.color as Color); + let color = data.color as number; let material: LineBasicMaterial = data.lineType === LineType.Dash ? new LineDashedMaterial({ color, dashSize: 6, gapSize: 6 }) @@ -144,7 +201,7 @@ export class ThreeVisualContext implements IVisualContext { private createVertexGeometry(data: VertexMeshData) { let buff = new BufferGeometry(); buff.setAttribute("position", new Float32BufferAttribute(data.positions, 3)); - let color = ThreeHelper.fromColor(data.color as Color); + let color = data.color as number; let material = new PointsMaterial({ size: data.size, sizeAttenuation: false, @@ -182,9 +239,11 @@ export class ThreeVisualContext implements IVisualContext { private displayModel(model: IModel) { let modelShape = model.shape(); if (modelShape === undefined) return; - let threeShape = new ThreeShape(modelShape, this.visual.highlighter); - threeShape.color = model.color; - threeShape.opacity = model.opacity; + let material = this.materialMap.get(model.materialId); + if (!material) { + throw new Error("Material not found"); + } + let threeShape = new ThreeShape(modelShape, this.visual.highlighter, material); threeShape.transform = model.matrix; this.visualShapes.add(threeShape); this._shapeModelMap.set(threeShape, model); @@ -196,10 +255,9 @@ export class ThreeVisualContext implements IVisualContext { if (shape === undefined) return; if (property === "matrix") { shape.transform = model.matrix; - } else if (property === "color") { - shape.color = model[property]; - } else if (property === "opacity") { - shape.opacity = model[property]; + } else if (property === "materialId") { + let material = this.materialMap.get(model.materialId)!; + shape.setFaceMaterial(material); } }; diff --git a/packages/chili-three/test/testDocument.ts b/packages/chili-three/test/testDocument.ts index a1f2bef2..dbaf3858 100644 --- a/packages/chili-three/test/testDocument.ts +++ b/packages/chili-three/test/testDocument.ts @@ -9,6 +9,8 @@ import { ISelection, ISerialize, IView, + Material, + ObservableCollection, PropertyChangedHandler, Serialized, } from "chili-core"; @@ -24,6 +26,7 @@ export class TestDocument implements IDocument, ISerialize { visual: ThreeVisual; rootNode: INodeLinkedList; activeView: IView | undefined; + materials: ObservableCollection = new ObservableCollection(); onPropertyChanged(handler: PropertyChangedHandler): void { throw new Error("Method not implemented."); } diff --git a/packages/chili-three/test/testEdge.ts b/packages/chili-three/test/testEdge.ts index 029c963f..b1a0dc08 100644 --- a/packages/chili-three/test/testEdge.ts +++ b/packages/chili-three/test/testEdge.ts @@ -1,6 +1,5 @@ import { Body, - Color, I18nKeys, ICurve, IDocument, @@ -50,7 +49,7 @@ export class TestEdge implements IEdge { shape: this, edges: { positions: [this.start.x, this.start.y, this.start.z, this.end.x, this.end.y, this.end.z], - color: Color.fromRGB(1.0, 0, 0), + color: 0xff0000, lineType: LineType.Solid, groups: [], }, diff --git a/packages/chili-three/test/three.test.ts b/packages/chili-three/test/three.test.ts index c094c008..78b8d5c8 100644 --- a/packages/chili-three/test/three.test.ts +++ b/packages/chili-three/test/three.test.ts @@ -1,7 +1,7 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { expect, jest, test } from "@jest/globals"; -import { GeometryModel, ShapeType, XY, XYZ } from "chili-core"; +import { GeometryModel, Material, ShapeType, XY, XYZ } from "chili-core"; import { TestDocument } from "./testDocument"; import { TestBody } from "./testEdge"; import { TestView } from "./testView"; @@ -14,6 +14,7 @@ import { TestView } from "./testView"; describe("three test", () => { let doc = new TestDocument(); + doc.materials.push(new Material(doc, "test", 0x00ff00)); let view = new TestView(doc, doc.visual.context); test("test view", () => { diff --git a/packages/chili-ui/src/dialog.ts b/packages/chili-ui/src/dialog.ts index 89a0fca6..d0b85124 100644 --- a/packages/chili-ui/src/dialog.ts +++ b/packages/chili-ui/src/dialog.ts @@ -1,9 +1,9 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { I18n, I18nKeys, IPropertyChanged, Property, SelectableItems } from "chili-core"; -import style from "./dialog.module.css"; import { button, div } from "./controls"; import { RadioGroup } from "./controls/itemsControl"; +import style from "./dialog.module.css"; export class Dialog { private constructor() {} diff --git a/packages/chili-ui/src/property/colorPorperty.module.css b/packages/chili-ui/src/property/colorPorperty.module.css index 26698d09..84f13645 100644 --- a/packages/chili-ui/src/property/colorPorperty.module.css +++ b/packages/chili-ui/src/property/colorPorperty.module.css @@ -1,4 +1,6 @@ .color { - width: 32px; + width: 64px; height: 32px; + border: 1px solid var(--border-color); + border-radius: 4px; } diff --git a/packages/chili-ui/src/property/colorProperty.ts b/packages/chili-ui/src/property/colorProperty.ts index bed43ce0..b8ca5150 100644 --- a/packages/chili-ui/src/property/colorProperty.ts +++ b/packages/chili-ui/src/property/colorProperty.ts @@ -14,7 +14,6 @@ export class ColorProperty extends PropertyBase { readonly document: IDocument, objects: any[], readonly property: Property, - readonly showTitle: boolean = true, ) { super(objects); this.input = input({ @@ -26,12 +25,10 @@ export class ColorProperty extends PropertyBase { this.appendChild( div( { className: commonStyle.panel }, - showTitle - ? label({ - className: commonStyle.propertyName, - textContent: localize(property.display), - }) - : "", + label({ + className: commonStyle.propertyName, + textContent: localize(property.display), + }), this.input, ), ); diff --git a/packages/chili-ui/src/property/input.module.css b/packages/chili-ui/src/property/input.module.css index ab21d4dc..fe0ede1d 100644 --- a/packages/chili-ui/src/property/input.module.css +++ b/packages/chili-ui/src/property/input.module.css @@ -12,4 +12,8 @@ border: 1px solid var(--border-color); border-radius: 4px; margin: 0px; + + &:focus { + border-color: var(--primary-color); + } } diff --git a/packages/chili-ui/src/property/input.ts b/packages/chili-ui/src/property/input.ts index a0652a7b..d4d1e81c 100644 --- a/packages/chili-ui/src/property/input.ts +++ b/packages/chili-ui/src/property/input.ts @@ -61,7 +61,6 @@ export class InputProperty extends PropertyBase { readonly document: IDocument, objects: any[], readonly property: Property, - readonly showTitle: boolean = true, ) { super(objects); this.converter = property.converter ?? this.getConverter(); @@ -69,12 +68,10 @@ export class InputProperty extends PropertyBase { this.append( div( { className: commonStyle.panel }, - showTitle - ? span({ - className: commonStyle.propertyName, - textContent: localize(property.display), - }) - : "", + span({ + className: commonStyle.propertyName, + textContent: localize(property.display), + }), input({ className: style.box, value: new Binding(objects[0], property.name, arrayConverter), diff --git a/packages/chili-ui/src/property/material/index.ts b/packages/chili-ui/src/property/material/index.ts new file mode 100644 index 00000000..4475d995 --- /dev/null +++ b/packages/chili-ui/src/property/material/index.ts @@ -0,0 +1,4 @@ +// Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. + +export * from "./materialDataContent"; +export * from "./materialEditor"; diff --git a/packages/chili-ui/src/property/material/materialDataContent.ts b/packages/chili-ui/src/property/material/materialDataContent.ts new file mode 100644 index 00000000..b2a5a735 --- /dev/null +++ b/packages/chili-ui/src/property/material/materialDataContent.ts @@ -0,0 +1,40 @@ +// Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. + +import { IDocument, Material, Observable } from "chili-core"; + +let count = 1; + +export class MaterialDataContent extends Observable { + private _editingMaterial: Material; + get editingMaterial(): Material { + return this._editingMaterial; + } + set editingMaterial(value: Material) { + this.setProperty("editingMaterial", value); + } + + constructor( + readonly document: IDocument, + readonly callback: (material: Material) => void, + editingMaterial: Material, + ) { + super(); + this._editingMaterial = editingMaterial; + } + + deleteMaterial() { + if (this.document.materials.length <= 1) return; + let tempMaterial = this.editingMaterial; + this.editingMaterial = this.document.materials.find((m) => m.id !== this.editingMaterial.id)!; + this.callback(this.editingMaterial); + this.document.materials.remove(tempMaterial); + } + + addMaterial() { + this.document.materials.push(new Material(this.document, `Material ${count++}`, 0xdddddd)); + } + + copyMaterial() { + this.document.materials.push(this.editingMaterial.clone()); + } +} diff --git a/packages/chili-ui/src/property/material/materialEditor.module.css b/packages/chili-ui/src/property/material/materialEditor.module.css new file mode 100644 index 00000000..1358afc4 --- /dev/null +++ b/packages/chili-ui/src/property/material/materialEditor.module.css @@ -0,0 +1,128 @@ +.root { + display: flex; + flex-direction: column; + position: absolute; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + background-color: var(--background-color); + top: 16px; + left: 16px; + padding: 16px; +} + +.title { + margin: 0px 6px; + display: flex; + flex-direction: row; + align-items: center; + + span { + font-size: medium; + flex: 1; + } + + svg { + width: 16px; + height: 16px; + padding: 6px; + margin: 0 3px; + + &:hover { + background-color: var(--hover-background-color); + } + } +} + +.materials { + display: flex; + flex-direction: row; + flex-wrap: wrap; + border-radius: 8px; + background-color: var(--panel-background-color); + border: 1px solid var(--border-color); + margin: 8px 0px; + padding: 4px; + height: 120px; +} + +.material { + border-radius: 8px; + width: 48px; + height: 48px; + border: 1px solid gray; + margin: 4px; +} + +.active { + border: 5px solid var(--primary-color); +} + +.editing { + display: flex; + flex-direction: row; + border-radius: 8px; + background-color: var(--panel-background-color); + border: 1px solid var(--border-color); + padding: 16px; + --delete-visiblity: hidden; + + .texture { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 4px; + border-radius: 8px; + position: relative; + + &:hover { + --delete-visiblity: visible; + } + + span { + margin-top: 4px; + } + + img { + width: 96px; + height: 96px; + border-radius: 4px; + } + + svg { + position: absolute; + background-color: rgba(255, 0, 0, 0.45); + border-radius: 50%; + padding: 4px; + top: 2px; + right: 2px; + width: 16px; + height: 16px; + visibility: var(--delete-visiblity); + } + } + + .properties { + display: flex; + flex-direction: column; + flex: 1; + margin-right: 16px; + } +} + +.bottom { + display: flex; + flex-direction: row; + margin-top: 8px; + + button { + width: 96px; + height: 28px; + margin-right: 8px; + border-radius: 6px; + border: 1px solid var(--border-color); + background-color: var(--panel-background-color); + &:hover { + background-color: var(--hover-background-color); + } + } +} diff --git a/packages/chili-ui/src/property/material/materialEditor.ts b/packages/chili-ui/src/property/material/materialEditor.ts new file mode 100644 index 00000000..bb4742a1 --- /dev/null +++ b/packages/chili-ui/src/property/material/materialEditor.ts @@ -0,0 +1,173 @@ +// Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. + +import { ColorConverter, IConverter, Material, Property, PubSub, Result, readFileAsync } from "chili-core"; +import { Binding, button, div, img, items, localize, span, svg } from "../../controls"; +import { appendProperty } from "../utils"; +import { MaterialDataContent } from "./materialDataContent"; +import style from "./materialEditor.module.css"; + +class ActiveStyleConverter implements IConverter { + constructor(readonly material: Material) {} + + convert(value: Material): Result { + return Result.success( + this.material === value ? `${style.material} ${style.active}` : style.material, + ); + } +} + +class UrlStringConverter implements IConverter { + convert(value: string): Result { + return Result.success(`url('${value}')`); + } +} + +export class MaterialEditor extends HTMLElement { + private editingControl: HTMLElement; + private readonly colorConverter = new ColorConverter(); + + constructor(readonly dataContent: MaterialDataContent) { + super(); + this.editingControl = div(); + this.initEditingControl(dataContent.editingMaterial); + this.append( + div( + { + className: style.root, + }, + div( + { className: style.title }, + span({ + textContent: localize("common.material"), + }), + svg({ + icon: "icon-plus", + onclick: () => { + this.dataContent.addMaterial(); + }, + }), + svg({ + icon: "icon-clone", + onclick: () => { + this.dataContent.copyMaterial(); + }, + }), + svg({ + icon: "icon-times", + onclick: () => { + this.dataContent.deleteMaterial(); + }, + }), + ), + items({ + className: style.materials, + sources: this.dataContent.document.materials, + template: (material: Material) => + span({ + className: new Binding( + this.dataContent, + "editingMaterial", + new ActiveStyleConverter(material), + ), + title: material.name, + style: { + backgroundColor: new Binding(material, "color", this.colorConverter), + background: new Binding(material, "texture", new UrlStringConverter()), + backgroundBlendMode: "multiply", + backgroundSize: "contain", + }, + onclick: () => { + this.dataContent.editingMaterial = material; + }, + ondblclick: () => { + this.dataContent.callback(material); + this.remove(); + }, + }), + }), + this.editingControl, + div( + { + className: style.bottom, + }, + button({ + textContent: localize("common.confirm"), + onclick: () => { + this.dataContent.callback(this.dataContent.editingMaterial); + this.remove(); + }, + }), + button({ + textContent: localize("common.cancel"), + onclick: () => { + this.remove(); + }, + }), + ), + ), + ); + } + + connectedCallback() { + this.dataContent.onPropertyChanged(this._onEditingMaterialChanged); + PubSub.default.sub("showProperties", this._handleShowProperty); + } + + disconnectedCallback() { + PubSub.default.remove("showProperties", this._handleShowProperty); + } + + private _handleShowProperty = () => { + this.remove(); + }; + + private _onEditingMaterialChanged = (property: keyof MaterialDataContent) => { + if (property !== "editingMaterial") return; + this.editingControl.firstChild?.remove(); + this.initEditingControl(this.dataContent.editingMaterial); + }; + + private initEditingControl(material: Material) { + let container = div({ + className: style.properties, + }); + this.editingControl.appendChild( + div( + { + className: style.editing, + }, + container, + div( + { + className: style.texture, + }, + img({ + style: { + 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; + }, + }), + span({ + textContent: localize("material.texture"), + }), + svg({ + icon: "icon-times", + onclick: () => { + material.texture = ""; + }, + }), + ), + ), + ); + + Property.getProperties(material).forEach((x) => { + appendProperty(container, this.dataContent.document, [material], x); + }); + } +} + +customElements.define("material-editor", MaterialEditor); diff --git a/packages/chili-ui/src/property/propertyView.module.css b/packages/chili-ui/src/property/propertyView.module.css index 77a9e976..d45c61d9 100644 --- a/packages/chili-ui/src/property/propertyView.module.css +++ b/packages/chili-ui/src/property/propertyView.module.css @@ -23,9 +23,40 @@ margin-top: 6px; } -.colorName { +.name { display: grid; grid-template-columns: auto 1fr; grid-gap: 10px; align-items: center; } + +.material { + display: flex; + flex-direction: row; + align-items: center; + margin-top: 4px; + + span { + font-size: 14px; + opacity: 0.75; + color: var(--color); + min-width: 72px; + flex: 0 1 auto; + } + + button { + flex: 1 1 auto; + font-size: 1rem; + padding: 4px; + outline: none; + background-color: transparent; + + border: 1px solid var(--border-color); + border-radius: 4px; + margin: 0px; + + &:hover { + background-color: var(--hover-background-color); + } + } +} diff --git a/packages/chili-ui/src/property/propertyView.ts b/packages/chili-ui/src/property/propertyView.ts index fadb0891..b8f11012 100644 --- a/packages/chili-ui/src/property/propertyView.ts +++ b/packages/chili-ui/src/property/propertyView.ts @@ -1,11 +1,11 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. import { - Color, GeometryModel, I18nKeys, IConverter, IDocument, + IModel, INode, IView, Property, @@ -13,12 +13,11 @@ import { } from "chili-core"; import { Expander } from "../components"; -import { div, label, localize, span } from "../controls"; -import { CheckProperty } from "./check"; -import { ColorProperty } from "./colorProperty"; +import { button, div, label, localize, span } from "../controls"; import { InputProperty } from "./input"; import { MatrixConverter } from "./matrixConverter"; import style from "./propertyView.module.css"; +import { appendProperty } from "./utils"; export class PropertyView extends HTMLElement { private panel = div({ className: style.panel }); @@ -60,29 +59,50 @@ export class PropertyView extends HTMLElement { private addDefault(document: IDocument, nodes: INode[]) { if (nodes.length === 0) return; - let nameProperty = Property.getProperty(nodes[0], "name")!; - let header: HTMLElement; let properties = div(); + let header = new InputProperty(document, nodes, Property.getProperty(nodes[0], "name")!); if (INode.isModelNode(nodes[0])) { - let colorProperty = Property.getProperty(nodes[0], "color")!; - let opacityProperty = Property.getProperty(nodes[0], "opacity")!; - header = div( - { className: style.colorName }, - new ColorProperty(document, nodes, colorProperty, false), - new InputProperty(document, nodes, nameProperty, false), - ); - this.appendProperty(properties, document, nodes, opacityProperty); - } else { - header = div( - { className: style.colorName }, - span({ textContent: localize("common.name") }), - new InputProperty(document, nodes, nameProperty, false), - ); + appendProperty(properties, document, nodes); + if (nodes.length === 1) { + this.appendMaterialProperty(document, nodes[0], properties); + } } this.panel.append(header, properties); } + private appendMaterialProperty(document: IDocument, model: IModel, properties: HTMLDivElement) { + const findMaterial = (id: string) => document.materials.find((x) => x.id === id)!; + properties.append( + div( + { + className: style.material, + }, + span({ + textContent: localize(Property.getProperty(model, "materialId")!.display), + }), + button({ + textContent: findMaterial(model.materialId).name, + onclick: (e) => { + PubSub.default.pub( + "editMaterial", + document, + findMaterial(model.materialId)!, + (material) => { + let button = e.target as HTMLButtonElement; + button.textContent = material.name; + model.materialId = material.id; + console.log(model.materialId); + + document.visual.update(); + }, + ); + }, + }), + ), + ); + } + private addBody(nodes: INode[], document: IDocument) { let bodies = nodes.filter((x) => INode.isModelNode(x)).map((x) => (x as GeometryModel).body); if (bodies.length === 0 || !this.isAllElementsOfTypeFirstElement(bodies)) return; @@ -90,7 +110,7 @@ export class PropertyView extends HTMLElement { this.panel.append(body); body.classList.add(style.expander); Property.getProperties(bodies[0]).forEach((x) => { - this.appendProperty(body.contenxtPanel, document, bodies, x); + appendProperty(body.contenxtPanel, document, bodies, x); }); } @@ -113,9 +133,9 @@ export class PropertyView extends HTMLElement { let transform = new Expander("properties.group.transform").addClass(style.expander); this.panel.append(transform); const addMatrix = (display: I18nKeys, converter: IConverter) => { - this.appendProperty(transform, document, nodes, { + appendProperty(transform, document, nodes, { name: "matrix", - display, + display: display, converter, }); }; @@ -125,21 +145,6 @@ export class PropertyView extends HTMLElement { addMatrix("model.scale", converters.scale); addMatrix("model.rotation", converters.rotate); } - - private appendProperty(container: HTMLElement, document: IDocument, objs: any[], prop?: Property) { - if (prop === undefined) return; - const propValue = (objs[0] as unknown as any)[prop.name]; - const type = typeof propValue; - if (type === "object" || type === "string" || type === "number") { - if (propValue instanceof Color) { - container.append(new ColorProperty(document, objs, prop)); - } else { - container.append(new InputProperty(document, objs, prop)); - } - } else if (type === "boolean") { - container.append(new CheckProperty(objs, prop)); - } - } } customElements.define("chili-property-view", PropertyView); diff --git a/packages/chili-ui/src/property/utils.ts b/packages/chili-ui/src/property/utils.ts new file mode 100644 index 00000000..e10f126c --- /dev/null +++ b/packages/chili-ui/src/property/utils.ts @@ -0,0 +1,20 @@ +// Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. + +import { IDocument, Property } from "chili-core"; +import { InputProperty } from "./input"; +import { CheckProperty } from "./check"; +import { ColorProperty } from "./colorProperty"; + +export function appendProperty(container: HTMLElement, document: IDocument, objs: any[], prop?: Property) { + if (prop === undefined) return; + const propValue = (objs[0] as unknown as any)[prop.name]; + const type = typeof propValue; + + if (prop.type === "color") { + container.append(new ColorProperty(document, objs, prop)); + } else if (type === "object" || type === "string" || type === "number") { + container.append(new InputProperty(document, objs, prop)); + } else if (type === "boolean") { + container.append(new CheckProperty(objs, prop)); + } +} diff --git a/packages/chili-ui/src/viewport/layoutViewport.ts b/packages/chili-ui/src/viewport/layoutViewport.ts index 068df5eb..7f7230e8 100644 --- a/packages/chili-ui/src/viewport/layoutViewport.ts +++ b/packages/chili-ui/src/viewport/layoutViewport.ts @@ -6,13 +6,18 @@ import { CollectionChangedArgs, CursorType, IApplication, + IDocument, IView, + Material, PubSub, } from "chili-core"; import { OKCancel } from "../components/okCancel"; import { Cursor } from "../cursor"; +import { MaterialEditor } from "../property/material"; +import { MaterialDataContent } from "../property/material/materialDataContent"; import style from "./layoutViewport.module.css"; import { Viewport } from "./viewport"; +import { items } from "../controls"; export class LayoutViewport extends HTMLElement { private readonly _selectionController: OKCancel; @@ -45,6 +50,7 @@ export class LayoutViewport extends HTMLElement { connectedCallback(): void { PubSub.default.sub("activeViewChanged", this._handleActiveViewChanged); PubSub.default.sub("showSelectionControl", this.showSelectionControl); + PubSub.default.sub("editMaterial", this._handleMaterialEdit); PubSub.default.sub("clearSelectionControl", this.clearSelectionControl); PubSub.default.sub("viewCursor", this._handleCursor); } @@ -52,6 +58,7 @@ export class LayoutViewport extends HTMLElement { disconnectedCallback(): void { PubSub.default.remove("activeViewChanged", this._handleActiveViewChanged); PubSub.default.remove("showSelectionControl", this.showSelectionControl); + PubSub.default.remove("editMaterial", this._handleMaterialEdit); PubSub.default.remove("clearSelectionControl", this.clearSelectionControl); PubSub.default.remove("viewCursor", this._handleCursor); } @@ -88,6 +95,15 @@ export class LayoutViewport extends HTMLElement { this._selectionController.setControl(undefined); this._selectionController.style.visibility = "hidden"; }; + + private _handleMaterialEdit = ( + document: IDocument, + editingMaterial: Material, + callback: (material: Material) => void, + ) => { + let context = new MaterialDataContent(document, callback, editingMaterial); + this.append(new MaterialEditor(context)); + }; } customElements.define("chili-viewport", LayoutViewport); diff --git a/packages/chili-ui/src/viewport/viewport.module.css b/packages/chili-ui/src/viewport/viewport.module.css new file mode 100644 index 00000000..3010bbfd --- /dev/null +++ b/packages/chili-ui/src/viewport/viewport.module.css @@ -0,0 +1,3 @@ +.root { + position: relative; +} diff --git a/packages/chili-ui/src/viewport/viewport.ts b/packages/chili-ui/src/viewport/viewport.ts index 846256c4..6886f37e 100644 --- a/packages/chili-ui/src/viewport/viewport.ts +++ b/packages/chili-ui/src/viewport/viewport.ts @@ -1,7 +1,8 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { IView, PubSub } from "chili-core"; +import { IView } from "chili-core"; import { Flyout } from "../components"; +import style from "./viewport.module.css"; export class Viewport extends HTMLElement { private _flyout: Flyout; @@ -9,31 +10,31 @@ export class Viewport extends HTMLElement { constructor(readonly view: IView) { super(); + this.className = style.root; this.initEvent(); this._flyout = new Flyout(); + } + + connectedCallback() { + this.appendChild(this._flyout); this.addEventListener("mousemove", this._handleFlyoutMove); - PubSub.default.sub("activeViewChanged", this._onActiveViewChanged); } - private _onActiveViewChanged = (view: IView | undefined) => { - if (view === this.view) { - document.body.appendChild(this._flyout); - } else { - this._flyout.remove(); - } - }; + disconnectedCallback() { + this._flyout.remove(); + this.removeEventListener("mousemove", this._handleFlyoutMove); + } private _handleFlyoutMove(e: MouseEvent) { if (this._flyout) { - this._flyout.style.top = e.clientY + "px"; - this._flyout.style.left = e.clientX + "px"; + this._flyout.style.top = e.offsetY + "px"; + this._flyout.style.left = e.offsetX + "px"; } } dispose() { this.removeEvents(); this.removeEventListener("mousemove", this._handleFlyoutMove); - PubSub.default.remove("activeViewChanged", this._onActiveViewChanged); } private initEvent() { diff --git a/packages/chili/src/application.ts b/packages/chili/src/application.ts index 2031d6b8..8b558046 100644 --- a/packages/chili/src/application.ts +++ b/packages/chili/src/application.ts @@ -77,22 +77,25 @@ export class Application extends Observable implements IApplication { async openDocument(id: string): Promise { let document = await Document.open(this, id); - if (!document) return undefined; - return await this.handleDocumentAndActiveView(() => document); + await this.createActiveView(document); + return document; } async newDocument(name: string): Promise { - return await this.handleDocumentAndActiveView(() => new Document(this, name))!; + let document = new Document(this, name); + await this.createActiveView(document); + return document; } - async loadDocument(data: Serialized): Promise { - return await this.handleDocumentAndActiveView(() => Document.load(this, data)); + async loadDocument(data: Serialized): Promise { + let document = Document.load(this, data); + await this.createActiveView(document); + return document; } - private async handleDocumentAndActiveView(proxy: () => IDocument) { - let document = proxy(); + private async createActiveView(document: IDocument | undefined) { + if (document === undefined) return undefined; let view = document.visual.createView("3d", Plane.XY); this.activeView = view; - return document; } } diff --git a/packages/chili/src/commands/application/newDocument.ts b/packages/chili/src/commands/application/newDocument.ts index 72746d82..ae1b0586 100644 --- a/packages/chili/src/commands/application/newDocument.ts +++ b/packages/chili/src/commands/application/newDocument.ts @@ -1,6 +1,6 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { IApplication, ICommand, command } from "chili-core"; +import { IApplication, ICommand, Material, command } from "chili-core"; let count = 1; @@ -11,6 +11,9 @@ let count = 1; }) export class NewDocument implements ICommand { async execute(app: IApplication): Promise { - app.newDocument(`undefined ${count++}`); + let document = await app.newDocument(`undefined ${count++}`); + let lightGray = new Material(document, "LightGray", 0xdedede); + let deepGray = new Material(document, "DeepGray", 0x898989); + document.materials.push(lightGray, deepGray); } } diff --git a/packages/chili/src/commands/application/openDocument.ts b/packages/chili/src/commands/application/openDocument.ts index bcf6eee6..b804d44f 100644 --- a/packages/chili/src/commands/application/openDocument.ts +++ b/packages/chili/src/commands/application/openDocument.ts @@ -16,7 +16,7 @@ export class OpenDocument implements ICommand { if (files.status === "success") { let json: Serialized = JSON.parse(files.value[0].data); let document = await app.loadDocument(json); - document.application.activeView?.cameraController.fitContent(); + document?.application.activeView?.cameraController.fitContent(); } }, "toast.excuting{0}", diff --git a/packages/chili/src/commands/modify/array.ts b/packages/chili/src/commands/modify/array.ts index b7558c32..0cdcb45d 100644 --- a/packages/chili/src/commands/modify/array.ts +++ b/packages/chili/src/commands/modify/array.ts @@ -2,12 +2,12 @@ import { AsyncController, - Config, IModel, INode, LineType, Matrix4, Transaction, + VisualConfig, XYZ, command, } from "chili-core"; @@ -54,7 +54,7 @@ export class Array extends MultistepCommand { { positions, lineType: LineType.Solid, - color: Config.instance.visual.temporaryEdgeColor, + color: VisualConfig.temporaryEdgeColor, groups: [], }, ]; diff --git a/packages/chili/src/commands/modify/transformedCommand.ts b/packages/chili/src/commands/modify/transformedCommand.ts index 735c1a00..05586251 100644 --- a/packages/chili/src/commands/modify/transformedCommand.ts +++ b/packages/chili/src/commands/modify/transformedCommand.ts @@ -2,7 +2,6 @@ import { AsyncController, - Config, EdgeMeshData, IModel, INode, @@ -11,6 +10,7 @@ import { Property, PubSub, Transaction, + VisualConfig, XYZ, } from "chili-core"; import { MultistepCommand } from "../multistepCommand"; @@ -37,13 +37,13 @@ export abstract class TransformedCommand extends MultistepCommand { return { positions, lineType: LineType.Solid, - color: Config.instance.visual.faceEdgeColor, + color: VisualConfig.defaultEdgeColor, groups: [], }; }; protected getTempLineData(start: XYZ, end: XYZ) { - return EdgeMeshData.from(start, end, Config.instance.visual.temporaryEdgeColor, LineType.Solid); + return EdgeMeshData.from(start, end, VisualConfig.temporaryEdgeColor, LineType.Solid); } protected override async beforeExecute(): Promise { diff --git a/packages/chili/src/commands/multistepCommand.ts b/packages/chili/src/commands/multistepCommand.ts index 49dd097c..885c892d 100644 --- a/packages/chili/src/commands/multistepCommand.ts +++ b/packages/chili/src/commands/multistepCommand.ts @@ -3,11 +3,11 @@ import { AsyncController, CancelableCommand, - Config, EdgeMeshData, LineType, Property, VertexMeshData, + VisualConfig, XYZ, } from "chili-core"; import { SnapedData } from "../snap"; @@ -68,15 +68,11 @@ export abstract class MultistepCommand extends CancelableCommand { } protected previewPoint(point: XYZ) { - return VertexMeshData.from( - point, - Config.instance.visual.editVertexSize, - Config.instance.visual.editVertexColor, - ); + return VertexMeshData.from(point, VisualConfig.editVertexSize, VisualConfig.editVertexColor); } protected previewLine(start: XYZ, end: XYZ) { - return EdgeMeshData.from(start, end, Config.instance.visual.temporaryEdgeColor, LineType.Dash); + return EdgeMeshData.from(start, end, VisualConfig.temporaryEdgeColor, LineType.Dash); } protected abstract getSteps(): IStep[]; diff --git a/packages/chili/src/document.ts b/packages/chili/src/document.ts index 90cafe14..580f7acf 100644 --- a/packages/chili/src/document.ts +++ b/packages/chili/src/document.ts @@ -18,18 +18,22 @@ import { NodeRecord, NodeSerializer, Observable, + ObservableCollection, PubSub, + Result, Serialized, Serializer, } from "chili-core"; import { Selection } from "./selection"; +import { Material } from "chili-core/src/material"; -const FILE_VERSIOM = "0.1.0"; +const FILE_VERSIOM = "0.1.1"; export class Document extends Observable implements IDocument { readonly visual: IVisual; readonly history: History; readonly selection: ISelection; + readonly materials: ObservableCollection = new ObservableCollection(); private _name: string; get name(): string { @@ -94,6 +98,7 @@ export class Document extends Observable implements IDocument { id: this.id, name: this.name, nodes: NodeSerializer.serialize(this.rootNode), + materials: this.materials.map((x) => Serializer.serializeObject(x)), }, }; return serialized; @@ -104,6 +109,8 @@ export class Document extends Observable implements IDocument { this.visual.dispose(); this.history.dispose(); this.selection.dispose(); + this.materials.forEach((x) => x.dispose()); + this.materials.clear(); this._rootNode?.removePropertyChanged(this.handleRootNodeNameChanged); this._rootNode?.dispose(); this._rootNode = undefined; @@ -147,13 +154,26 @@ export class Document extends Observable implements IDocument { return; } let document = this.load(application, data); - Logger.info(`document: ${document.name} opened`); + if (document !== undefined) { + Logger.info(`document: ${document.name} opened`); + } return document; } - static load(app: IApplication, data: Serialized) { + static load(app: IApplication, data: Serialized): IDocument | undefined { + if ((data as any).version !== FILE_VERSIOM) { + alert( + "The file version has been upgraded, no compatibility treatment was done in the development phase", + ); + return undefined; + } let document = new Document(app, data.properties["name"], data.properties["id"]); document.history.disabled = true; + document.materials.push( + ...data.properties["materials"].map((x: Serialized) => + Serializer.deserializeObject(document, x), + ), + ); document.setRootNode(NodeSerializer.deserialize(document, data.properties["nodes"])); document.history.disabled = false; return document; diff --git a/packages/chili/src/snap/axisSnap.ts b/packages/chili/src/snap/axisSnap.ts index e15e3faf..074764b0 100644 --- a/packages/chili/src/snap/axisSnap.ts +++ b/packages/chili/src/snap/axisSnap.ts @@ -1,6 +1,6 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { Config, EdgeMeshData, IView, LineType, Plane, XYZ } from "chili-core"; +import { EdgeMeshData, IView, LineType, Plane, VisualConfig, XYZ } from "chili-core"; import { ISnapper, MouseAndDetected, SnapedData } from "."; @@ -37,7 +37,7 @@ export class AxisSnap implements ISnapper { let lineDats = EdgeMeshData.from( this.point, this.point.add(this.direction.multiply(dist)), - Config.instance.visual.temporaryEdgeColor, + VisualConfig.temporaryEdgeColor, LineType.Dash, ); let id = view.document.visual.context.displayShapeMesh(lineDats); diff --git a/packages/chili/src/snap/objectSnap.ts b/packages/chili/src/snap/objectSnap.ts index 9eac3db5..0d8a0d8b 100644 --- a/packages/chili/src/snap/objectSnap.ts +++ b/packages/chili/src/snap/objectSnap.ts @@ -14,6 +14,7 @@ import { PubSub, ShapeType, VertexMeshData, + VisualConfig, VisualShapeData, VisualState, XYZ, @@ -109,8 +110,8 @@ export class ObjectSnap implements ISnapper { this.hilighted(view, shape.shapes); let data = VertexMeshData.from( shape.point!, - Config.instance.visual.hintVertexSize, - Config.instance.visual.hintVertexColor, + VisualConfig.hintVertexSize, + VisualConfig.hintVertexColor, ); this._hintVertex = [ view.document.visual.context, @@ -161,8 +162,8 @@ export class ObjectSnap implements ISnapper { private showCircleCenter(curve: ICircle, view: IView, shape: VisualShapeData) { let temporary = VertexMeshData.from( curve.center, - Config.instance.visual.hintVertexSize, - Config.instance.visual.hintVertexColor, + VisualConfig.hintVertexSize, + VisualConfig.hintVertexColor, ); let id = view.document.visual.context.displayShapeMesh(temporary); this._invisibleInfos.set(shape, { diff --git a/packages/chili/src/snap/snapEventHandler/snapEventHandler.ts b/packages/chili/src/snap/snapEventHandler/snapEventHandler.ts index 46074cf9..df775e7d 100644 --- a/packages/chili/src/snap/snapEventHandler/snapEventHandler.ts +++ b/packages/chili/src/snap/snapEventHandler/snapEventHandler.ts @@ -13,6 +13,7 @@ import { Result, ShapeType, VertexMeshData, + VisualConfig, XYZ, } from "chili-core"; @@ -170,8 +171,8 @@ export abstract class SnapEventHandler implements IEventHandler { if (point) { let data = VertexMeshData.from( point, - Config.instance.visual.temporaryVertexSize, - Config.instance.visual.temporaryVertexColor, + VisualConfig.temporaryVertexSize, + VisualConfig.temporaryVertexColor, ); this._tempPoint = this.document.visual.context.displayShapeMesh(data); } diff --git a/packages/chili/src/snap/tracking/objectTracking.ts b/packages/chili/src/snap/tracking/objectTracking.ts index da2c2989..ccaa69ca 100644 --- a/packages/chili/src/snap/tracking/objectTracking.ts +++ b/packages/chili/src/snap/tracking/objectTracking.ts @@ -1,6 +1,6 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { Config, IDocument, IView, VertexMeshData } from "chili-core"; +import { IDocument, IView, VertexMeshData, VisualConfig } from "chili-core"; import { SnapedData } from ".."; import { Axis } from "./axis"; @@ -81,8 +81,8 @@ export class ObjectTracking { private addTrackingPoint(snap: SnapedData, document: IDocument, snaps: SnapeInfo[]) { let data = VertexMeshData.from( snap.point!, - Config.instance.visual.trackingVertexSize, - Config.instance.visual.trackingVertexColor, + VisualConfig.trackingVertexSize, + VisualConfig.trackingVertexColor, ); let pointId = document.visual.context.displayShapeMesh(data); snaps.push({ shapeId: pointId, snap }); diff --git a/packages/chili/src/snap/tracking/trackingSnap.ts b/packages/chili/src/snap/tracking/trackingSnap.ts index 3a97226f..de1b1868 100644 --- a/packages/chili/src/snap/tracking/trackingSnap.ts +++ b/packages/chili/src/snap/tracking/trackingSnap.ts @@ -13,6 +13,7 @@ import { PubSub, Ray, ShapeType, + VisualConfig, XY, XYZ, } from "chili-core"; @@ -93,12 +94,7 @@ export class TrackingSnap implements ISnapper { if (normal === undefined) return undefined; let distance = vector.length() * 1e10; let newEnd = start.add(normal.multiply(distance > 1e20 ? 1e20 : distance)); - let lineDats = EdgeMeshData.from( - start, - newEnd, - Config.instance.visual.temporaryEdgeColor, - LineType.Dash, - ); + let lineDats = EdgeMeshData.from(start, newEnd, VisualConfig.temporaryEdgeColor, LineType.Dash); return view.document.visual.context.displayShapeMesh(lineDats); } diff --git a/public/iconfont.js b/public/iconfont.js index 5ed27d01..58f0075d 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