diff --git a/packages/chili-core/src/visual/view.ts b/packages/chili-core/src/visual/view.ts index 9909c4c1..213e82d3 100644 --- a/packages/chili-core/src/visual/view.ts +++ b/packages/chili-core/src/visual/view.ts @@ -27,13 +27,13 @@ export interface IView extends IPropertyChanged, IDisposable { up(): XYZ; toImage(): string; direction(): XYZ; - rotate(dx: number, dy: number): void; - zoomIn(): void; - zoomOut(): void; + rotate(dx: number, dy: number): Promise; + zoomIn(): Promise; + zoomOut(): Promise; rayAt(mx: number, my: number): Ray; screenToWorld(mx: number, my: number): XYZ; worldToScreen(point: XYZ): XY; - fitContent(): void; + fitContent(): Promise; resize(width: number, heigth: number): void; setDom(element: HTMLElement): void; close(): void; diff --git a/packages/chili-three/src/threeView.ts b/packages/chili-three/src/threeView.ts index 572a49a4..e8711aa4 100644 --- a/packages/chili-three/src/threeView.ts +++ b/packages/chili-three/src/threeView.ts @@ -166,7 +166,10 @@ export class ThreeView extends Observable implements IView { private createCamera() { let camera: PerspectiveCamera | OrthographicCamera; - let aspect = this.width! / (this.height ?? 1); + let aspect = this.width! / this.height!; + if (Number.isNaN(aspect)) { + aspect = 1; + } if (this.cameraType === CameraType.perspective) { camera = new PerspectiveCamera(45, aspect, 1, 1e6); } else { @@ -243,6 +246,7 @@ export class ThreeView extends Observable implements IView { } this._controls = new CameraControls(this.camera, this.renderer.domElement); this._controls.draggingSmoothTime = 0.06; + this._controls.smoothTime = 0.1; this._controls.dollyToCursor = true; this._controls.polarRotateSpeed = 0.8; this._controls.azimuthRotateSpeed = 0.8; @@ -312,6 +316,9 @@ export class ThreeView extends Observable implements IView { } resize(width: number, height: number) { + if (height < 0.00001) { + return; + } this._renderer.setSize(width, height); this._camera = this.createCamera(); if (this._controls) { @@ -326,7 +333,7 @@ export class ThreeView extends Observable implements IView { this.update(); } - fitContent(): void { + async fitContent() { let box = new Box3(); let shapes = this.document.selection.getSelectedNodes().filter((x) => x instanceof VisualNode); if (shapes.length === 0) { @@ -339,7 +346,10 @@ export class ThreeView extends Observable implements IView { } let sphere = new Sphere(); box.getBoundingSphere(sphere); - this._controls?.fitToSphere(sphere, true); + if (sphere.radius < 1) { + sphere.radius = 1; + } + await this._controls?.fitToSphere(sphere, true); } get width() { @@ -350,23 +360,23 @@ export class ThreeView extends Observable implements IView { return this._dom?.clientHeight; } - rotate(dx: number, dy: number): void { - this._controls?.rotate(dx, dy); + async rotate(dx: number, dy: number) { + await this._controls?.rotate(dx, dy); } - zoomIn(): void { + async zoomIn() { if (this.cameraType === CameraType.orthographic) { - this._controls?.zoom(this.camera.zoom * 0.3); + await this._controls?.zoom(this.camera.zoom * 0.3); } else { - this._controls?.dolly(this.cameraPosition.distanceTo(this.cameraTarget) * 0.2); + await this._controls?.dolly(this.cameraPosition.distanceTo(this.cameraTarget) * 0.2); } } - zoomOut(): void { + async zoomOut() { if (this.cameraType === CameraType.orthographic) { - this._controls?.zoom(-this.camera.zoom * 0.3); + await this._controls?.zoom(-this.camera.zoom * 0.3); } else { - this._controls?.dolly(this.cameraPosition.distanceTo(this.cameraTarget) * -0.2); + await this._controls?.dolly(this.cameraPosition.distanceTo(this.cameraTarget) * -0.2); } } diff --git a/packages/chili-three/src/viewGizmo.ts b/packages/chili-three/src/viewGizmo.ts index 95fbe0b3..54b14bd6 100644 --- a/packages/chili-three/src/viewGizmo.ts +++ b/packages/chili-three/src/viewGizmo.ts @@ -27,7 +27,7 @@ export interface Axis { size: number; position: Vector3; color: string[]; - line?: number; + lineWidth?: number; label?: string; } @@ -55,6 +55,8 @@ export class ViewGizmo extends HTMLElement { this.style.right = "20px"; this.style.borderRadius = "100%"; this.style.cursor = "pointer"; + this.style.userSelect = "none"; + this.style.webkitUserSelect = "none"; } private _initCanvas() { @@ -67,7 +69,7 @@ export class ViewGizmo extends HTMLElement { return canvas; } - private _initAxes() { + private _initAxes(): Axis[] { return [ { axis: "x", @@ -75,7 +77,7 @@ export class ViewGizmo extends HTMLElement { position: new Vector3(), size: options.bubbleSizePrimary, color: options.colors.x, - line: options.lineWidth, + lineWidth: options.lineWidth, label: "X", }, { @@ -84,7 +86,7 @@ export class ViewGizmo extends HTMLElement { position: new Vector3(), size: options.bubbleSizePrimary, color: options.colors.y, - line: options.lineWidth, + lineWidth: options.lineWidth, label: "Y", }, { @@ -93,7 +95,7 @@ export class ViewGizmo extends HTMLElement { position: new Vector3(), size: options.bubbleSizePrimary, color: options.colors.z, - line: options.lineWidth, + lineWidth: options.lineWidth, label: "Z", }, { @@ -210,7 +212,7 @@ export class ViewGizmo extends HTMLElement { for (let axis of axes) { let color = this.getAxisColor(axis); this.drawCircle(axis.position, axis.size, color); - this.drawLine(this._center, axis.position, color, axis.line); + this.drawLine(this._center, axis.position, color, axis.lineWidth); this.drawLabel(axis); } } diff --git a/packages/chili-ui/src/home/home.ts b/packages/chili-ui/src/home/home.ts index 96746dda..1f4e4ff5 100644 --- a/packages/chili-ui/src/home/home.ts +++ b/packages/chili-ui/src/home/home.ts @@ -128,7 +128,7 @@ export class Home extends HTMLElement { "showPermanent", async () => { let document = await this.app.openDocument(item.id); - document?.application.activeView?.fitContent(); + await document?.application.activeView?.fitContent(); }, "toast.excuting{0}", I18n.translate("command.document.open"), diff --git a/packages/chili-ui/src/viewport/layoutViewport.ts b/packages/chili-ui/src/viewport/layoutViewport.ts index 48c113fc..f33df26c 100644 --- a/packages/chili-ui/src/viewport/layoutViewport.ts +++ b/packages/chili-ui/src/viewport/layoutViewport.ts @@ -31,7 +31,7 @@ export class LayoutViewport extends HTMLElement { app.views.onCollectionChanged(this._handleViewCollectionChanged); } - private _handleViewCollectionChanged = (args: CollectionChangedArgs) => { + private readonly _handleViewCollectionChanged = (args: CollectionChangedArgs) => { if (args.action === CollectionAction.add) { args.items.forEach((view) => { this.createViewport(view); @@ -62,7 +62,7 @@ export class LayoutViewport extends HTMLElement { PubSub.default.remove("viewCursor", this._handleCursor); } - private _handleCursor = (type: CursorType) => { + private readonly _handleCursor = (type: CursorType) => { this.style.cursor = Cursor.get(type); }; @@ -75,7 +75,7 @@ export class LayoutViewport extends HTMLElement { return viewport; } - private _handleActiveViewChanged = (view: IView | undefined) => { + private readonly _handleActiveViewChanged = (view: IView | undefined) => { this._viewports.forEach((v) => { if (v.view === view) { v.classList.remove(style.hidden); @@ -85,18 +85,18 @@ export class LayoutViewport extends HTMLElement { }); }; - private showSelectionControl = (controller: AsyncController) => { + private readonly showSelectionControl = (controller: AsyncController) => { this._selectionController.setControl(controller); this._selectionController.style.visibility = "visible"; this._selectionController.style.zIndex = "1000"; }; - private clearSelectionControl = () => { + private readonly clearSelectionControl = () => { this._selectionController.setControl(undefined); this._selectionController.style.visibility = "hidden"; }; - private _handleMaterialEdit = ( + private readonly _handleMaterialEdit = ( document: IDocument, editingMaterial: Material, callback: (material: Material) => void, diff --git a/packages/chili-ui/src/viewport/viewport.module.css b/packages/chili-ui/src/viewport/viewport.module.css index 3010bbfd..2eb9d42b 100644 --- a/packages/chili-ui/src/viewport/viewport.module.css +++ b/packages/chili-ui/src/viewport/viewport.module.css @@ -1,3 +1,55 @@ .root { position: relative; } + +.viewControls { + position: absolute; + height: 100%; + top: 0; + right: 16px; + z-index: 999; + pointer-events: none; + padding-top: 160px; + + svg { + border: none; + background: transparent; + width: 22px; + height: 22px; + padding: 8px; + border-radius: 7.5px; + + &:hover { + background: rgba(255, 255, 255, 0.74); + } + + &:active { + background: rgba(255, 255, 255, 0.96); + } + } + + .actived { + background: rgba(255, 255, 255, 0.56); + } + + .border { + border: 1px solid #8f8f8f; + background: rgba(204, 204, 204, 0.75); + border-radius: 10px; + margin: 1px 0px; + padding: 2px; + pointer-events: all; + display: flex; + flex-direction: column; + overflow: hidden; + + div { + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 7.5px; + } + } +} diff --git a/packages/chili-ui/src/viewport/viewport.ts b/packages/chili-ui/src/viewport/viewport.ts index b108cd48..5d015060 100644 --- a/packages/chili-ui/src/viewport/viewport.ts +++ b/packages/chili-ui/src/viewport/viewport.ts @@ -1,9 +1,20 @@ // Copyright 2022-2023 the Chili authors. All rights reserved. AGPL-3.0 license. -import { CameraType, IView } from "chili-core"; -import { button, div, Flyout, localize } from "../components"; +import { Binding, CameraType, IConverter, IView, Result } from "chili-core"; +import { div, Flyout, svg } from "../components"; import style from "./viewport.module.css"; +class CameraConverter implements IConverter { + constructor(readonly type: CameraType) {} + + convert(value: CameraType): Result { + if (value === this.type) { + return Result.ok(style.actived); + } + return Result.ok(""); + } +} + export class Viewport extends HTMLElement { private readonly _flyout: Flyout; private readonly _eventCaches: [keyof HTMLElementEventMap, (e: any) => void][] = []; @@ -13,6 +24,84 @@ export class Viewport extends HTMLElement { this.className = style.root; this.initEvent(); this._flyout = new Flyout(); + this.render(); + } + + private render() { + this.append( + div( + { + className: style.viewControls, + onpointerdown(ev) { + ev.stopPropagation(); + }, + onclick: (e) => { + e.stopPropagation(); + }, + }, + div( + { + className: style.border, + }, + div( + { + className: new Binding( + this.view, + "cameraType", + new CameraConverter(CameraType.orthographic), + ), + }, + svg({ + icon: "icon-orthographic", + onclick: (e) => { + e.stopPropagation(); + this.view.cameraType = CameraType.orthographic; + }, + }), + ), + div( + { + className: new Binding( + this.view, + "cameraType", + new CameraConverter(CameraType.perspective), + ), + }, + svg({ + icon: "icon-perspective", + onclick: (e) => { + e.stopPropagation(); + this.view.cameraType = CameraType.perspective; + }, + }), + ), + ), + div( + { + className: style.border, + }, + svg({ + icon: "icon-fitcontent", + onclick: async (e) => { + e.stopPropagation(); + await this.view.fitContent(); + }, + }), + svg({ + icon: "icon-zoomin", + onclick: () => { + this.view.zoomIn(); + }, + }), + svg({ + icon: "icon-zoomout", + onclick: () => { + this.view.zoomOut(); + }, + }), + ), + ), + ); } connectedCallback() { diff --git a/packages/chili/src/application.ts b/packages/chili/src/application.ts index 7825cd22..dbb057c2 100644 --- a/packages/chili/src/application.ts +++ b/packages/chili/src/application.ts @@ -128,7 +128,7 @@ export class Application implements IApplication { for (const file of opens) { let json: Serialized = JSON.parse(await file.text()); await this.loadDocument(json); - this.activeView?.fitContent(); + await this.activeView?.fitContent(); } }, "toast.excuting{0}", diff --git a/packages/chili/src/commands/application/openDocument.ts b/packages/chili/src/commands/application/openDocument.ts index 256f810f..173eaa6d 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.isOk) { let json: Serialized = JSON.parse(files.value[0].data); let document = await app.loadDocument(json); - document?.application.activeView?.fitContent(); + await document?.application.activeView?.fitContent(); } }, "toast.excuting{0}", diff --git a/packages/chili/src/utils.ts b/packages/chili/src/utils.ts index f3b0f8a4..d8844f94 100644 --- a/packages/chili/src/utils.ts +++ b/packages/chili/src/utils.ts @@ -10,7 +10,7 @@ export async function importFiles(application: IApplication, files: File[] | Fil await Transaction.excuteAsync(document, "import model", async () => { await document.application.dataExchange.import(document, files); }); - document.application.activeView?.fitContent(); + await document.application.activeView?.fitContent(); }, "toast.excuting{0}", I18n.translate("command.import"), diff --git a/public/iconfont.js b/public/iconfont.js index 137f33a9..bac878fd 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 z,a,m,t,c,i=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)}}z=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?i(l,h.firstChild):h.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(z,0):(a=function(){document.removeEventListener("DOMContentLoaded",a,!1),z()},document.addEventListener("DOMContentLoaded",a,!1)):document.attachEvent&&(m=z,t=v.document,c=!1,p(),t.onreadystatechange=function(){"complete"==t.readyState&&(t.onreadystatechange=null,o())})}function o(){c||(c=!0,m())}function p(){try{t.documentElement.doScroll("left")}catch(h){return void setTimeout(p,50)}o()}}(window); \ No newline at end of file +window._iconfont_svg_string_3585225='',(v=>{var l=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var z,a,m,t,i,c=function(l,h){h.parentNode.insertBefore(l,h)};if(l&&!v.__iconfont__svg__cssinject__){v.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}z=function(){var l,h=document.createElement("div");h.innerHTML=v._iconfont_svg_string_3585225,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(l=document.body).firstChild?c(h,l.firstChild):l.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(z,0):(a=function(){document.removeEventListener("DOMContentLoaded",a,!1),z()},document.addEventListener("DOMContentLoaded",a,!1)):document.attachEvent&&(m=z,t=v.document,i=!1,p(),t.onreadystatechange=function(){"complete"==t.readyState&&(t.onreadystatechange=null,o())})}function o(){i||(i=!0,m())}function p(){try{t.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}o()}})(window); \ No newline at end of file