initial commit
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.vscode
|
||||||
|
|
||||||
|
node_modules
|
||||||
2
.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
APS_CLIENT_ID = 'ulPmu8QxBzlIgSGqJODqdi47OEZHGpra0263lsk9aVvmZ6YH'
|
||||||
|
APS_CLIENT_SECRET = '3FvoT0y8roixHoGDrDjNfvnphPAli1neRrJlbJYZoch4BA9Lr6F4n519MC3FNbCO'
|
||||||
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.vscode
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
**/.vscode
|
||||||
|
**/node_modules
|
||||||
31
Dockerfile
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# check=skip=SecretsUsedInArgOrEnv
|
||||||
|
# ===== Build Vue =====
|
||||||
|
FROM node:22 AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /frontend
|
||||||
|
COPY wwwroot/ibms_ems/package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY wwwroot/ibms_ems ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ===== Backend =====
|
||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# copy Vue build
|
||||||
|
COPY --from=frontend-builder /frontend/dist ./wwwroot/ibms_ems/dist
|
||||||
|
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
PORT=8080 \
|
||||||
|
APS_CLIENT_ID= \
|
||||||
|
APS_CLIENT_SECRET=
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
7
Dockerfile.dev
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
FROM node:22
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安裝 backend deps
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
6
README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
`npm i` 下載包
|
||||||
|
|
||||||
|
`cd ./wwwroot/ibms_ems`
|
||||||
|
`npm i` 下載包
|
||||||
|
|
||||||
|
利用 `docker compose -f docker-compose.dev.yml up` 啟動整個專案
|
||||||
16
config.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
let { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_BUCKET, PORT } = process.env;
|
||||||
|
if (!APS_CLIENT_ID || !APS_CLIENT_SECRET) {
|
||||||
|
console.warn('Missing some of the environment variables.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
APS_BUCKET = APS_BUCKET || `${APS_CLIENT_ID.toLowerCase()}-basic-app`;
|
||||||
|
PORT = PORT || 8080;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
APS_CLIENT_ID,
|
||||||
|
APS_CLIENT_SECRET,
|
||||||
|
APS_BUCKET,
|
||||||
|
PORT
|
||||||
|
};
|
||||||
25
docker-compose.dev.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- PORT=8080
|
||||||
|
command: sh -c "npm run start"
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: node:22
|
||||||
|
working_dir: /app
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
volumes:
|
||||||
|
- ./wwwroot/ibms_ems:/app
|
||||||
|
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
|
||||||
12
docker-compose.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=8080
|
||||||
|
- APS_CLIENT_ID=${APS_CLIENT_ID}
|
||||||
|
- APS_CLIENT_SECRET=${APS_CLIENT_SECRET}
|
||||||
1336
package-lock.json
generated
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "ibms_ems",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@aps_sdk/authentication": "^1.0.1",
|
||||||
|
"@aps_sdk/model-derivative": "^1.2.1",
|
||||||
|
"@aps_sdk/oss": "^1.3.3",
|
||||||
|
"dotenv": "^17.4.1",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"express-formidable": "^1.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
routes/auth.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { getViewerToken } = require('../services/aps.js');
|
||||||
|
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
router.get('/api/auth/token', async function (req, res, next) {
|
||||||
|
try {
|
||||||
|
res.json(await getViewerToken());
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
39
server.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const { PORT } = require('./config.js');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
// 🔧 URL rewrite 必須在所有路由之前執行(對應 Vite dev proxy 的行為)
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.startsWith('/forge/api')) {
|
||||||
|
req.url = req.url.replace(/^\/forge\/api/, '/api');
|
||||||
|
req.path = req.path.replace(/^\/forge\/api/, '/api');
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const distPath = path.join(__dirname, 'wwwroot/ibms_ems/dist');
|
||||||
|
app.use(express.static(distPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👉 API routes(在 rewrite 之後)
|
||||||
|
app.use(require('./routes/auth.js'));
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
// Vue Router history mode (Express 5 catch-all syntax)
|
||||||
|
app.get('/{*path}', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'wwwroot/ibms_ems/dist', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 👉 Dev:只跑 API,不管前端
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('API server running...');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server listening on port ${PORT}...`);
|
||||||
|
});
|
||||||
25
services/aps.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const { AuthenticationClient, Scopes } = require('@aps_sdk/authentication');
|
||||||
|
const { OssClient, Region, PolicyKey } = require('@aps_sdk/oss');
|
||||||
|
const { ModelDerivativeClient, View, OutputType } = require('@aps_sdk/model-derivative');
|
||||||
|
const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_BUCKET } = require('../config.js');
|
||||||
|
|
||||||
|
const authenticationClient = new AuthenticationClient();
|
||||||
|
const ossClient = new OssClient();
|
||||||
|
const modelDerivativeClient = new ModelDerivativeClient();
|
||||||
|
|
||||||
|
const service = module.exports = {};
|
||||||
|
|
||||||
|
async function getInternalToken() {
|
||||||
|
const credentials = await authenticationClient.getTwoLeggedToken(APS_CLIENT_ID, APS_CLIENT_SECRET, [
|
||||||
|
Scopes.DataRead,
|
||||||
|
Scopes.DataCreate,
|
||||||
|
Scopes.DataWrite,
|
||||||
|
Scopes.BucketCreate,
|
||||||
|
Scopes.BucketRead
|
||||||
|
]);
|
||||||
|
return credentials.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
service.getViewerToken = async () => {
|
||||||
|
return await authenticationClient.getTwoLeggedToken(APS_CLIENT_ID, APS_CLIENT_SECRET, [Scopes.ViewablesRead]);
|
||||||
|
};
|
||||||
24
wwwroot/ibms_ems/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
14
wwwroot/ibms_ems/index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<!-- <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ibms_ems</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.js"></script>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1430
wwwroot/ibms_ems/package-lock.json
generated
Normal file
23
wwwroot/ibms_ems/package.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "ibms_ems",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
|
"three": "^0.183.2",
|
||||||
|
"vue": "^3.5.32"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.5",
|
||||||
|
"vite": "^8.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
wwwroot/ibms_ems/public/favicon.svg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
24
wwwroot/ibms_ems/public/icons.svg
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||||
|
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||||
|
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||||
|
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
189
wwwroot/ibms_ems/src/App.vue
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<script setup>
|
||||||
|
import background from "@/assets/bg_tech.jpg";
|
||||||
|
import titleLogo from "@/assets/title.png";
|
||||||
|
import ForgeViewer from "@/components/ForgeViewer.vue";
|
||||||
|
import Info from "@/components/Info.vue";
|
||||||
|
import { ref, useTemplateRef, watch } from "vue";
|
||||||
|
import btn01B from "@/assets/btn_01_b.png";
|
||||||
|
import btn01R from "@/assets/btn_01_r.png";
|
||||||
|
import btn02B from "@/assets/btn_02_b.png";
|
||||||
|
import btn02R from "@/assets/btn_02_r.png";
|
||||||
|
import btn03B from "@/assets/btn_03_b.png";
|
||||||
|
import btn03R from "@/assets/btn_03_r.png";
|
||||||
|
import btn04B from "@/assets/btn_04_b.png";
|
||||||
|
import btn04R from "@/assets/btn_04_r.png";
|
||||||
|
import btn05B from "@/assets/btn_05_b.png";
|
||||||
|
import btn05R from "@/assets/btn_05_r.png";
|
||||||
|
import btn06B from "@/assets/btn_06_b.png";
|
||||||
|
import btn06R from "@/assets/btn_06_r.png";
|
||||||
|
import btn07B from "@/assets/btn_07_b.png";
|
||||||
|
import btn07R from "@/assets/btn_07_r.png";
|
||||||
|
import btn08B from "@/assets/btn_08_b.png";
|
||||||
|
import btn08R from "@/assets/btn_08_r.png";
|
||||||
|
import line01 from "@/assets/line_01.png";
|
||||||
|
import line02 from "@/assets/line_02.png";
|
||||||
|
import line03 from "@/assets/line_03.png";
|
||||||
|
import line04 from "@/assets/line_04.png";
|
||||||
|
import line05 from "@/assets/line_05.png";
|
||||||
|
import line06 from "@/assets/line_06.png";
|
||||||
|
import line07 from "@/assets/line_07.png";
|
||||||
|
import line08 from "@/assets/line_08.png";
|
||||||
|
import changeCameraPosition from "@/utils/changeCameraPosition.js";
|
||||||
|
import changeCameraView from "@/utils/changeCameraView.js";
|
||||||
|
import ForgeLabel from "@/components/ForgeLabel.vue";
|
||||||
|
|
||||||
|
const left_data = ref([
|
||||||
|
{
|
||||||
|
title: "POWER DEMAND",
|
||||||
|
normalImg: btn01B,
|
||||||
|
hoverImg: btn01R,
|
||||||
|
forgeID: 72,
|
||||||
|
line: line01,
|
||||||
|
tag: "TAG_Electricity_meter",
|
||||||
|
arcSide: -1,
|
||||||
|
cameraDistance: 5,
|
||||||
|
value: 13.25,
|
||||||
|
content: [
|
||||||
|
"Power Demand: 12.8 MW",
|
||||||
|
"Capacity Usage: 56%",
|
||||||
|
"Contract Capacity: 22.8 MW",
|
||||||
|
"Energy Consumption Today: 86,500 kWh",
|
||||||
|
"Monthly Peak Demand: 18.6 MW",
|
||||||
|
"30-Min Demand Forecast: 16.2 MW"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "CARBON REDUCTION",
|
||||||
|
normalImg: btn02B,
|
||||||
|
hoverImg: btn02R,
|
||||||
|
forgeID: 181,
|
||||||
|
line: line02,
|
||||||
|
tag: "",
|
||||||
|
arcSide: -1,
|
||||||
|
cameraDistance: 5,
|
||||||
|
value: 1280
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "ENVIRONMENT INDEX",
|
||||||
|
normalImg: btn03B,
|
||||||
|
hoverImg: btn03R,
|
||||||
|
forgeID: 30,
|
||||||
|
line: line03,
|
||||||
|
tag: "",
|
||||||
|
arcSide: -1,
|
||||||
|
cameraDistance: 5,
|
||||||
|
value: 856
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "PUE",
|
||||||
|
normalImg: btn04B,
|
||||||
|
hoverImg: btn04R,
|
||||||
|
forgeID: 41,
|
||||||
|
line: line04,
|
||||||
|
tag: "ROOM_Information_Data_Center",
|
||||||
|
arcSide: 1,
|
||||||
|
cameraDistance: 5,
|
||||||
|
value: 1.22
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const right_data = ref([
|
||||||
|
{
|
||||||
|
title: "AI PREDICTED YIELD",
|
||||||
|
normalImg: btn05B,
|
||||||
|
hoverImg: btn05R,
|
||||||
|
forgeID: 51,
|
||||||
|
line: line05,
|
||||||
|
tag: "ROOM_OFFICE",
|
||||||
|
arcSide: 1,
|
||||||
|
cameraDistance: 2,
|
||||||
|
value: 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "REAL-TIME THROUGHPUT",
|
||||||
|
normalImg: btn06B,
|
||||||
|
hoverImg: btn06R,
|
||||||
|
forgeID: 61,
|
||||||
|
line: line06,
|
||||||
|
tag: "TAG_Chiller-Water_Cooled",
|
||||||
|
arcSide: -1,
|
||||||
|
cameraDistance: 10,
|
||||||
|
value: 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "REAL-TIME",
|
||||||
|
normalImg: btn07B,
|
||||||
|
hoverImg: btn07R,
|
||||||
|
forgeID: 71,
|
||||||
|
line: line07,
|
||||||
|
tag: "TAG_solder_paste_screen_printer",
|
||||||
|
arcSide: -1,
|
||||||
|
cameraDistance: 5,
|
||||||
|
value: 98
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "SAFETY & ALARMS",
|
||||||
|
normalImg: btn08B,
|
||||||
|
hoverImg: btn08R,
|
||||||
|
forgeID: 81,
|
||||||
|
line: line08,
|
||||||
|
tag: "",
|
||||||
|
arcSide: -1,
|
||||||
|
cameraDistance: 5,
|
||||||
|
value: 1.5
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const forgeViewerRef = useTemplateRef("forgeViewerRef");
|
||||||
|
|
||||||
|
const forgeLabelRef = useTemplateRef("forgeLabelRef");
|
||||||
|
|
||||||
|
const imgSrcActive = ref(null);
|
||||||
|
const onClick = (item) => {
|
||||||
|
|
||||||
|
if (item.tag === "") return
|
||||||
|
imgSrcActive.value = item;
|
||||||
|
|
||||||
|
//TODO: 切換視角
|
||||||
|
console.log(forgeLabelRef.value);
|
||||||
|
changeCameraPosition(
|
||||||
|
forgeViewerRef.value.forgeViewer,
|
||||||
|
forgeLabelRef.value.tagDom,
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.log(forgeViewerRef.value.Viewpoints[7]);
|
||||||
|
// changeCameraView(forgeViewerRef.value.forgeViewer, forgeViewerRef.value.Viewpoints[7].data);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => forgeViewerRef.value?.tagData,
|
||||||
|
(tagList) => {
|
||||||
|
if (!tagList || tagList.length === 0) return;
|
||||||
|
|
||||||
|
left_data.value = left_data.value.map((item) => {
|
||||||
|
const tagInfo = tagList.find((d) => d.tag === item.tag);
|
||||||
|
return tagInfo ? { ...item, ...tagInfo } : item;
|
||||||
|
});
|
||||||
|
|
||||||
|
right_data.value = right_data.value.map((item) => {
|
||||||
|
const tagInfo = tagList.find((d) => d.tag === item.tag);
|
||||||
|
return tagInfo ? { ...item, ...tagInfo } : item;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-screen h-screen overflow-hidden relative flex items-center justify-center bg-cover"
|
||||||
|
:style="{ backgroundImage: `url('${background}')` }">
|
||||||
|
<img :src="titleLogo" alt="Title Logo" class="absolute top-0 left-0 m-4 w-full h-[106px]" />
|
||||||
|
|
||||||
|
<Info position="left" :data="left_data" :imgSrcActive="imgSrcActive?.forgeID" :onClick="onClick" />
|
||||||
|
<ForgeViewer ref="forgeViewerRef" :forge-ids="forgeIDs" />
|
||||||
|
<Info position="right" :data="right_data" :imgSrcActive="imgSrcActive?.forgeID" :onClick="onClick" />
|
||||||
|
|
||||||
|
<ForgeLabel ref="forgeLabelRef" v-if="imgSrcActive" :data="imgSrcActive" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
BIN
wwwroot/ibms_ems/src/assets/background.jpg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
wwwroot/ibms_ems/src/assets/bg_tech.jpg
Normal file
|
After Width: | Height: | Size: 559 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_01_b.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_01_r.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_02_b.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_02_r.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_03_b.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_03_r.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_04_b.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_04_r.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_05_b.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_05_r.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_06_b.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_06_r.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_07_b.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_07_r.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_08_b.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
wwwroot/ibms_ems/src/assets/btn_08_r.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
wwwroot/ibms_ems/src/assets/kv_pro3c_tech.jpg
Normal file
|
After Width: | Height: | Size: 655 KiB |
BIN
wwwroot/ibms_ems/src/assets/light_circle.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
wwwroot/ibms_ems/src/assets/line_01.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
wwwroot/ibms_ems/src/assets/line_02.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
wwwroot/ibms_ems/src/assets/line_03.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
wwwroot/ibms_ems/src/assets/line_04.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
wwwroot/ibms_ems/src/assets/line_05.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
wwwroot/ibms_ems/src/assets/line_06.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
wwwroot/ibms_ems/src/assets/line_07.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
wwwroot/ibms_ems/src/assets/line_08.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
wwwroot/ibms_ems/src/assets/title.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
98
wwwroot/ibms_ems/src/components/ForgeLabel.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<div id="tag" ref="tag" class="hud-container z-50 hidden">
|
||||||
|
<div class="hud-panel p-5 text-white text-3xl flex justify-center items-center">
|
||||||
|
<span class="tag_name mr-5">
|
||||||
|
{{ data.title }}
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-col items-start">
|
||||||
|
<span v-for="(value, index) in data.content" :key="index" class="text-xl">
|
||||||
|
{{ value }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hud-connector">
|
||||||
|
<div class="hud-connector-line"></div>
|
||||||
|
<div class="hud-connector-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineExpose, defineProps } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tag = ref(null);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
tagDom: tag
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:root {
|
||||||
|
--hud-cyan: #35f5ff;
|
||||||
|
--hud-cyan-soft: rgba(53, 245, 255, 0.4);
|
||||||
|
--hud-bg: #1b1f26;
|
||||||
|
--hud-panel-bg: #2a303a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-panel {
|
||||||
|
position: relative;
|
||||||
|
background: radial-gradient(circle at top left, #3b4656 0%, #1b1f26 55%, #11151b 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(53, 245, 255, 0.15),
|
||||||
|
0 0 18px rgba(53, 245, 255, 0.35),
|
||||||
|
inset 0 0 12px rgba(0, 0, 0, 0.7);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-panel::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(53, 245, 255, 0.35);
|
||||||
|
box-shadow: 0 0 12px rgba(53, 245, 255, 0.4);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-connector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-connector-line {
|
||||||
|
width: 2px;
|
||||||
|
height: 18px;
|
||||||
|
background: linear-gradient(to bottom, rgba(53, 245, 255, 0.7), rgba(53, 245, 255, 0));
|
||||||
|
box-shadow: 0 0 6px rgba(53, 245, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-connector-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: radial-gradient(circle, #ffffff 0%, #35f5ff 40%, #0b1518 100%);
|
||||||
|
box-shadow:
|
||||||
|
0 0 8px rgba(53, 245, 255, 0.9),
|
||||||
|
0 0 16px rgba(53, 245, 255, 0.7);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
wwwroot/ibms_ems/src/components/ForgeViewer.vue
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<Loader v-if="loading" />
|
||||||
|
<div class="w-[1106px] h-[966px] absolute z-10 top-20 bg-cover overflow-hidden"
|
||||||
|
:style="{ backgroundImage: `url('${CircleBg}')` }">
|
||||||
|
<div id="preview" ref="forgeViewerDOM" class="forge-ellipse translate-y-[15px] w-[1106px] h-[966px]"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- <button class="absolute z-50 top-5 left-5 bg-white" @click.prevent="onClick">切換視角</button>
|
||||||
|
<div class="w-screen h-screen ">
|
||||||
|
<div id="preview" ref="forgeViewerDOM"></div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, ref, toRaw, watch } from "vue";
|
||||||
|
import changeCameraPosition from "@/utils/changeCameraPosition.js";
|
||||||
|
import CircleBg from "@/assets/light_circle.png";
|
||||||
|
import getTag from "@/utils/getTag.js";
|
||||||
|
import Loader from "./Loader.vue";
|
||||||
|
|
||||||
|
|
||||||
|
const MODEL_URN = 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6dWxwbXU4cXhiemxpZ3NncWpvZHFkaTQ3b2V6aGdwcmEwMjYzbHNrOWF2dm16NnloLWRlbW8vJUUzJTgwJTkwUFJPM0NfU21hcnRfRmFjdG9yeSVFMyU4MCU5MTA2MDIubndk'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
forgeIds: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const forgeViewerDOM = ref(null);
|
||||||
|
const forgeViewer = ref(null);
|
||||||
|
const tagData = ref([]);
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
const getAccessToken = async (callback) => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/forge/api/auth/token');
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(await resp.text());
|
||||||
|
}
|
||||||
|
const { access_token, expires_in } = await resp.json();
|
||||||
|
callback(access_token, expires_in);
|
||||||
|
} catch (err) {
|
||||||
|
alert('Could not obtain access token. See the console for more details.');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initViewer = (container) => {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
Autodesk.Viewing.Initializer({ env: 'AutodeskProduction', getAccessToken }, function () {
|
||||||
|
const config = {
|
||||||
|
extensions: ['Autodesk.DocumentBrowser']
|
||||||
|
};
|
||||||
|
const viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
|
||||||
|
Autodesk.Viewing.Private.InitParametersSetting.alpha = true;
|
||||||
|
|
||||||
|
viewer.start();
|
||||||
|
resolve(viewer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findViewpoints(node, result = []) {
|
||||||
|
if (!node) return result;
|
||||||
|
|
||||||
|
const data = node.data;
|
||||||
|
|
||||||
|
// 判斷是否是 view / viewpoint
|
||||||
|
|
||||||
|
// console.log('Node:', data, data.type, data.role, data.camera, data.name, data.isViewpoint);
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
(data.type === 'view' && data.role === '3d')
|
||||||
|
|
||||||
|
) {
|
||||||
|
result.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
node.children.forEach(child => findViewpoints(child, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const Viewpoints = ref([]);
|
||||||
|
const loadModel = (viewer, urn) => {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
function onDocumentLoadSuccess(doc) {
|
||||||
|
viewer.setGroundShadow(false);
|
||||||
|
viewer.impl.renderer().setClearAlpha(0); //clear alpha channel
|
||||||
|
viewer.impl.glrenderer().setClearColor(0xffffff, 0); //set transparent background, color code does not matter
|
||||||
|
viewer.impl.invalidate(true); //trigger rendering
|
||||||
|
|
||||||
|
const root = doc.getRoot();
|
||||||
|
|
||||||
|
const views = findViewpoints(root);
|
||||||
|
|
||||||
|
console.log('Viewpoints:', views);
|
||||||
|
Viewpoints.value = views;
|
||||||
|
|
||||||
|
resolve(viewer.loadDocumentNode(doc, doc.getRoot().getDefaultGeometry()));
|
||||||
|
}
|
||||||
|
function onDocumentLoadFailure(code, message, errors) {
|
||||||
|
reject({ code, message, errors });
|
||||||
|
}
|
||||||
|
viewer.setLightPreset(0);
|
||||||
|
Autodesk.Viewing.Document.load('urn:' + urn, onDocumentLoadSuccess, onDocumentLoadFailure);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 記錄相機初始位置
|
||||||
|
const initialCameraPosition = ref(null);
|
||||||
|
const initialCameraTarget = ref(null);
|
||||||
|
|
||||||
|
|
||||||
|
watch(forgeViewerDOM, async (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
try {
|
||||||
|
const viewer = await initViewer(newVal);
|
||||||
|
|
||||||
|
forgeViewer.value = toRaw(viewer);
|
||||||
|
await loadModel(viewer, MODEL_URN);
|
||||||
|
|
||||||
|
// ⭐ 讓模型自動縮放到橢圓框內可見
|
||||||
|
viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, async () => {
|
||||||
|
|
||||||
|
// viewer.fitToView();
|
||||||
|
|
||||||
|
// 取得相機原本的位置與 target
|
||||||
|
const nav = viewer.navigation;
|
||||||
|
const cameraPos = nav.getPosition().clone();
|
||||||
|
const cameraTarget = nav.getTarget().clone();
|
||||||
|
|
||||||
|
initialCameraPosition.value = cameraPos
|
||||||
|
initialCameraTarget.value = cameraTarget;
|
||||||
|
|
||||||
|
|
||||||
|
// 根據你的 UI(橢圓框大小)調整
|
||||||
|
const scaleFactor = 1.3; // ⭐ 微調用
|
||||||
|
const direction = cameraPos.clone().sub(cameraTarget).normalize();
|
||||||
|
const distance = cameraPos.distanceTo(cameraTarget) * scaleFactor;
|
||||||
|
const newPos = cameraTarget.clone().add(direction.multiplyScalar(distance));
|
||||||
|
viewer.navigation.setView(newPos, cameraTarget); // 再置中
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const tags = await getTag(viewer);
|
||||||
|
tagData.value = tags;
|
||||||
|
console.log('Tags:', tags);
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
alert('Failed to load Forge Viewer. See the console for more details.');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (forgeViewer.value) {
|
||||||
|
changeCameraPosition(forgeViewer.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
forgeViewer,
|
||||||
|
initialCameraPosition,
|
||||||
|
initialCameraTarget,
|
||||||
|
tagData,
|
||||||
|
Viewpoints
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.forge-ellipse {
|
||||||
|
|
||||||
|
/* 橢圓裁切 */
|
||||||
|
clip-path: circle(483px at 50% 50%);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.adsk-viewing-viewer,
|
||||||
|
.adsk-viewing-canvas,
|
||||||
|
.adsk-viewing-overlay,
|
||||||
|
.adsk-viewing-overlay div {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
wwwroot/ibms_ems/src/components/Info.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="twMerge(
|
||||||
|
'flex flex-col items-center justify-center h-full absolute -bottom-10 z-30 ',
|
||||||
|
position === 'left' ? 'left-5' : 'right-5'
|
||||||
|
)">
|
||||||
|
<template v-for="(d, i) in data" :key="d.title">
|
||||||
|
<div class="my-6 relative" @click.prevent="() => onClick(d)">
|
||||||
|
<div class="relative">
|
||||||
|
<img :src="imgSrcActive === d.forgeID ? d.hoverImg : d.normalImg" alt="Image" class="w-[360px] h-[150px]">
|
||||||
|
<img :src="d.line" alt="Line" :class="twMerge(
|
||||||
|
'absolute ',
|
||||||
|
position === 'left' ? 'left-full' : 'right-full',
|
||||||
|
i % 4 === 3 ? 'top-0' : 'top-1/2'
|
||||||
|
)">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="absolute bottom-1/4 translate-y-1/2 right-1/5 mb-1 font-bold text-white text-5xl">{{ d.value }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps } from 'vue';
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
position: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validator: value => ['left', 'right'].includes(value)
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
imgSrcActive: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
onClick: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
22
wwwroot/ibms_ems/src/components/InfoCard.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<img :src="btn" alt="Image" class="w-[360px] h-[150px]">
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import InfoCardCircle from './InfoCardCircle.vue'
|
||||||
|
import btn from '@/assets/btn_01_b.png'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: [Number, String],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background: #0b1020;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
27
wwwroot/ibms_ems/src/components/Loader.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full absolute z-40 top-0 left-0 bg-black opacity-50 flex items-center justify-center ">
|
||||||
|
</div>
|
||||||
|
<div class="loader text-red absolute z-50 "></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
/* HTML: <div class="loader"></div> */
|
||||||
|
.loader {
|
||||||
|
width: 150px;
|
||||||
|
height: 15px;
|
||||||
|
--c:#90f4f5 50%,#0000 0;
|
||||||
|
background:
|
||||||
|
linear-gradient( 90deg,var(--c)) 0 0,
|
||||||
|
linear-gradient(-90deg,var(--c)) 0 0;
|
||||||
|
background-size: 20px 100%;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
animation: l10 1s infinite linear;
|
||||||
|
}
|
||||||
|
@keyframes l10 {
|
||||||
|
100% {background-position: -20px 0,20px 0}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
5
wwwroot/ibms_ems/src/main.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
||||||
5
wwwroot/ibms_ems/src/style.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-btech: #121063;
|
||||||
|
}
|
||||||
237
wwwroot/ibms_ems/src/utils/changeCameraPosition.js
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import * as TWEEN from "@tweenjs/tween.js";
|
||||||
|
import { toRaw } from "vue";
|
||||||
|
|
||||||
|
const THREE = window.THREE;
|
||||||
|
let rafId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 相機沿弧線飛行
|
||||||
|
*/
|
||||||
|
function flyArc(param) {
|
||||||
|
const {
|
||||||
|
viewer,
|
||||||
|
fromPos,
|
||||||
|
fromTarget,
|
||||||
|
toPos,
|
||||||
|
toTarget,
|
||||||
|
label,
|
||||||
|
returnToOriginal = false,
|
||||||
|
duration = 3000,
|
||||||
|
item,
|
||||||
|
arcSide = -1,
|
||||||
|
} = param;
|
||||||
|
const nav = viewer.navigation;
|
||||||
|
|
||||||
|
// 中間控制點(弧線)
|
||||||
|
const distance = fromPos.distanceTo(toPos);
|
||||||
|
|
||||||
|
const forward = toPos.clone().sub(fromPos).normalize();
|
||||||
|
|
||||||
|
const up = new THREE.Vector3(0, 1, 0);
|
||||||
|
|
||||||
|
const side = new THREE.Vector3().crossVectors(forward, up).normalize();
|
||||||
|
|
||||||
|
const midPos = fromPos.clone().lerp(toPos, 0.5);
|
||||||
|
|
||||||
|
// ⭐ 控制左右方向
|
||||||
|
midPos.add(side.multiplyScalar(distance * arcSide * 0.7));
|
||||||
|
|
||||||
|
const curve = new THREE.QuadraticBezierCurve3(fromPos, midPos, toPos);
|
||||||
|
|
||||||
|
const tweenObj = { t: 0 };
|
||||||
|
|
||||||
|
const tween = new TWEEN.Tween(tweenObj)
|
||||||
|
.to({ t: 1 }, duration)
|
||||||
|
.easing(TWEEN.Easing.Cubic.InOut)
|
||||||
|
.onStart(() => {
|
||||||
|
if (returnToOriginal) {
|
||||||
|
label.classList.remove("flex");
|
||||||
|
label.classList.add("hidden");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onUpdate(() => {
|
||||||
|
const point = curve.getPoint(tweenObj.t);
|
||||||
|
nav.setPosition(point);
|
||||||
|
|
||||||
|
const target = fromTarget.clone().lerp(toTarget, tweenObj.t);
|
||||||
|
nav.setTarget(target);
|
||||||
|
})
|
||||||
|
.onComplete(() => {
|
||||||
|
console.log("飛行完成", label.classList, returnToOriginal);
|
||||||
|
|
||||||
|
if (!returnToOriginal) {
|
||||||
|
label.querySelector(".tag_name").textContent = item.title;
|
||||||
|
label.classList.remove("hidden");
|
||||||
|
label.classList.add("flex");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
|
||||||
|
function animate(time) {
|
||||||
|
if (rafId) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
rafId = null;
|
||||||
|
}
|
||||||
|
rafId = requestAnimationFrame(animate);
|
||||||
|
tween.update(time);
|
||||||
|
}
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得物件 BoundingBox
|
||||||
|
*/
|
||||||
|
function getObjectBoundingBox(viewer, dbId) {
|
||||||
|
const THREE = window.THREE;
|
||||||
|
const model = viewer.model;
|
||||||
|
const tree = model.getInstanceTree();
|
||||||
|
const fragList = model.getFragmentList();
|
||||||
|
const bbox = new THREE.Box3();
|
||||||
|
|
||||||
|
tree.enumNodeFragments(
|
||||||
|
dbId,
|
||||||
|
(fragId) => {
|
||||||
|
const fragBox = new THREE.Box3();
|
||||||
|
fragList.getWorldBounds(fragId, fragBox);
|
||||||
|
bbox.union(fragBox);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 計算相機最佳距離(物件越大,相機越遠)
|
||||||
|
*/
|
||||||
|
function computeCameraOffset(viewer, bbox, distanceMultiplier = 7) {
|
||||||
|
const THREE = window.THREE;
|
||||||
|
const nav = viewer.navigation;
|
||||||
|
|
||||||
|
const center = bbox.getCenter(new THREE.Vector3());
|
||||||
|
const size = bbox.getSize(new THREE.Vector3());
|
||||||
|
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
|
||||||
|
// FOV
|
||||||
|
const fov = (nav.getVerticalFov() * Math.PI) / 180;
|
||||||
|
|
||||||
|
let distance = maxDim / (2 * Math.tan(fov / 2));
|
||||||
|
|
||||||
|
// 拉遠一點
|
||||||
|
distance *= distanceMultiplier;
|
||||||
|
|
||||||
|
// ⭐ 固定斜上角方向(超重要)
|
||||||
|
const direction = new THREE.Vector3(-0.35, -0.5, 1).normalize();
|
||||||
|
|
||||||
|
// ⭐ 相機位置
|
||||||
|
const position = center.clone().add(direction.multiplyScalar(distance));
|
||||||
|
|
||||||
|
return {
|
||||||
|
position,
|
||||||
|
target: center,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function showByName(v, id) {
|
||||||
|
v.hideAll();
|
||||||
|
v.show(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取得樓層
|
||||||
|
function isolateFloor(viewer, minZ, maxZ) {
|
||||||
|
const model = viewer.model;
|
||||||
|
const tree = model.getInstanceTree();
|
||||||
|
const fragList = model.getFragmentList();
|
||||||
|
const THREE = window.THREE;
|
||||||
|
|
||||||
|
const visibleDbIds = [];
|
||||||
|
|
||||||
|
tree.enumNodeChildren(
|
||||||
|
tree.getRootId(),
|
||||||
|
(dbId) => {
|
||||||
|
if (tree.getChildCount(dbId) !== 0) return;
|
||||||
|
|
||||||
|
const bbox = new THREE.Box3();
|
||||||
|
|
||||||
|
tree.enumNodeFragments(dbId, (fragId) => {
|
||||||
|
const fragBox = new THREE.Box3();
|
||||||
|
fragList.getWorldBounds(fragId, fragBox);
|
||||||
|
bbox.union(fragBox);
|
||||||
|
});
|
||||||
|
|
||||||
|
const centerZ = bbox.getCenter(new THREE.Vector3()).z;
|
||||||
|
|
||||||
|
if (centerZ >= minZ && centerZ <= maxZ) {
|
||||||
|
visibleDbIds.push(dbId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
viewer.hideAll();
|
||||||
|
|
||||||
|
viewer.setGhosting(false); // ⭐ 關鍵
|
||||||
|
viewer.isolate(visibleDbIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主功能:飛到物件 → 再飛回原點
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function changeCameraPosition(forgeViewer, label, item) {
|
||||||
|
const viewer = toRaw(forgeViewer);
|
||||||
|
|
||||||
|
console.log("切換視角到物件:", item);
|
||||||
|
showByName(viewer, item.parent);
|
||||||
|
|
||||||
|
// isolateFloor(viewer, -3000, 0); // 單位看你的模型
|
||||||
|
|
||||||
|
const nav = viewer.navigation;
|
||||||
|
|
||||||
|
// ⭐ 記錄相機初始位置與 target
|
||||||
|
const initialPos = nav.getPosition().clone();
|
||||||
|
const initialTarget = nav.getTarget().clone();
|
||||||
|
|
||||||
|
// ⭐ 取得物件資訊
|
||||||
|
const bbox = getObjectBoundingBox(viewer, item.forgeID);
|
||||||
|
const center = bbox.getCenter(new THREE.Vector3());
|
||||||
|
|
||||||
|
console.log("物件中心:", center);
|
||||||
|
|
||||||
|
// ⭐ 計算最佳觀察點
|
||||||
|
const { position, target } = computeCameraOffset(viewer, bbox, item.cameraDistance);
|
||||||
|
|
||||||
|
// ⭐ 第 1 段:飛到物件
|
||||||
|
const toTarget = {
|
||||||
|
viewer,
|
||||||
|
fromPos: initialPos,
|
||||||
|
fromTarget: initialTarget,
|
||||||
|
toPos: position,
|
||||||
|
toTarget: target,
|
||||||
|
label,
|
||||||
|
returnToOriginal: false,
|
||||||
|
duration: 5000,
|
||||||
|
item,
|
||||||
|
arcSide: item.arcSide,
|
||||||
|
};
|
||||||
|
flyArc(toTarget);
|
||||||
|
|
||||||
|
// ⭐ 第 2 段:停 0.5 秒後飛回原點
|
||||||
|
setTimeout(() => {
|
||||||
|
const toOriginal = {
|
||||||
|
viewer,
|
||||||
|
fromPos: position,
|
||||||
|
fromTarget: target,
|
||||||
|
toPos: initialPos,
|
||||||
|
toTarget: initialTarget,
|
||||||
|
label,
|
||||||
|
returnToOriginal: true,
|
||||||
|
duration: 5000,
|
||||||
|
item,
|
||||||
|
arcSide: -(item.arcSide),
|
||||||
|
};
|
||||||
|
flyArc(toOriginal);
|
||||||
|
viewer.showAll(); // 恢復顯示所有物件
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
64
wwwroot/ibms_ems/src/utils/changeCameraView.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import * as TWEEN from "@tweenjs/tween.js";
|
||||||
|
|
||||||
|
const THREE = window.THREE;
|
||||||
|
let currentTween = null;
|
||||||
|
|
||||||
|
function flyArc(viewer, cameraArr, duration = 3000) {
|
||||||
|
// 🛑 停掉前一個動畫
|
||||||
|
if (currentTween) {
|
||||||
|
TWEEN.remove(currentTween);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPos = viewer.navigation.getPosition().clone();
|
||||||
|
const startTarget = viewer.navigation.getTarget().clone();
|
||||||
|
|
||||||
|
const endPos = new THREE.Vector3(cameraArr[0], cameraArr[1], cameraArr[2]);
|
||||||
|
|
||||||
|
const endTarget = new THREE.Vector3(cameraArr[3], cameraArr[4], cameraArr[5]);
|
||||||
|
|
||||||
|
const distance = startPos.distanceTo(endPos);
|
||||||
|
|
||||||
|
// 🎯 建立「控制點」(讓路徑變弧形)
|
||||||
|
const mid = startPos.clone().lerp(endPos, 0.5);
|
||||||
|
mid.z += distance * 1.5; // 弧度高度
|
||||||
|
|
||||||
|
// 👉 建立 Bezier 曲線
|
||||||
|
const curve = new THREE.QuadraticBezierCurve3(startPos, mid, endPos);
|
||||||
|
|
||||||
|
const obj = { t: 0 };
|
||||||
|
|
||||||
|
const tween = new TWEEN.Tween(obj)
|
||||||
|
.to({ t: 1 }, duration)
|
||||||
|
.easing(TWEEN.Easing.Cubic.InOut)
|
||||||
|
.onUpdate(() => {
|
||||||
|
const t = obj.t;
|
||||||
|
|
||||||
|
// 🎯 沿曲線取點
|
||||||
|
const pos = curve.getPoint(t);
|
||||||
|
|
||||||
|
// 👉 target 用線性插值(穩定)
|
||||||
|
const target = startTarget.clone().lerp(endTarget, t);
|
||||||
|
|
||||||
|
viewer.navigation.setView(pos, target);
|
||||||
|
})
|
||||||
|
.onComplete(() => {
|
||||||
|
currentTween = null;
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
|
||||||
|
currentTween = tween;
|
||||||
|
|
||||||
|
// 🧠 確保 Tween 有在跑
|
||||||
|
function animate(time) {
|
||||||
|
if (currentTween) {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
currentTween.update(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function changeCameraView(forgeViewer, Viewpoint) {
|
||||||
|
flyArc(forgeViewer, Viewpoint.camera);
|
||||||
|
}
|
||||||
50
wwwroot/ibms_ems/src/utils/getTag.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
export default async function getTag(viewer) {
|
||||||
|
const tree = viewer.model.getInstanceTree();
|
||||||
|
const result = [];
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
tree.enumNodeChildren(
|
||||||
|
tree.getRootId(),
|
||||||
|
(dbId) => {
|
||||||
|
tasks.push(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
viewer.getProperties(dbId, (props) => {
|
||||||
|
const tag = props.properties.find((p) =>
|
||||||
|
p.displayName.includes("tag_id"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
let currentId = dbId;
|
||||||
|
let parentId = tree.getNodeParentId(currentId);
|
||||||
|
|
||||||
|
while (parentId !== tree.getRootId()) {
|
||||||
|
currentId = parentId;
|
||||||
|
parentId = tree.getNodeParentId(currentId);
|
||||||
|
}
|
||||||
|
console.log("最上層父節點 ID:", currentId);
|
||||||
|
result.push({
|
||||||
|
forgeID: dbId,
|
||||||
|
tag: tag.displayValue,
|
||||||
|
parent: currentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(tasks);
|
||||||
|
|
||||||
|
const uniqueResult = [];
|
||||||
|
result.forEach((item) => {
|
||||||
|
if (!uniqueResult.some((i) => i.tag === item.tag)) {
|
||||||
|
uniqueResult.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return uniqueResult;
|
||||||
|
}
|
||||||
8
wwwroot/ibms_ems/tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./src/**/*.{vue,js}"
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
}
|
||||||
26
wwwroot/ibms_ems/vite.config.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
"@ASSET": fileURLToPath(new URL("./src/assets", import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: true, // 🔥 關鍵
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/forge": {
|
||||||
|
target: process.env.NODE_ENV === "production" ? "http://backend:8080" : "http://localhost:8080", // Backend server
|
||||||
|
changeOrigin: true, // Ensure the request appears to come from the frontend server
|
||||||
|
rewrite: (path) => path.replace(/^\/forge/, ""), // Optional: Remove '/api' prefix
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||