diff --git a/.env b/.env
new file mode 100644
index 0000000..17e0677
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+NEXT_PUBLIC_DULA_NET_ADMIN_TASKS=/api/admin/tasks
+NEXT_PUBLIC_DULA_NET_TASK=/api/task/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6bc8e2e..cb90a36 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,22 @@ The format is based on [Keep a Changelog](https://github.com/olivierlacan/keep-a
### Removed
+## [2.1.0] - 2024-02-18
+
+### Added
+
+- Dula-net demo page. (https://github.com/yushiang-demo/pano-to-mesh/pull/73)
+
+### Changed
+
+- Remove 2d layout coords sorting to support occluded wall. (https://github.com/yushiang-demo/pano-to-mesh/pull/73)
+
+### Fixed
+
+### Removed
+
+- Remove MIT license. (https://github.com/yushiang-demo/pano-to-mesh/pull/74)
+
## [2.0.1] - 2023-12-17
### Added
@@ -197,7 +213,8 @@ Codes without pull requests won't be recorded.
### Removed
-[unreleased]: https://github.com/yushiang-demo/PanoToMesh/compare/v2.0.1...HEAD
+[unreleased]: https://github.com/yushiang-demo/PanoToMesh/compare/v2.1.0...HEAD
+[2.1.0]: https://github.com/yushiang-demo/PanoToMesh/compare/v2.0.1...v2.1.0
[2.0.1]: https://github.com/yushiang-demo/PanoToMesh/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/yushiang-demo/PanoToMesh/compare/v1.4.1...v2.0.0
[1.4.1]: https://github.com/yushiang-demo/PanoToMesh/compare/v1.4.0...v1.4.1
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index d2a31db..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2023 yushiang-demo
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/apps/admin.js b/apps/admin.js
new file mode 100644
index 0000000..17a2e32
--- /dev/null
+++ b/apps/admin.js
@@ -0,0 +1,108 @@
+import Upload from "../components/Upload";
+import GridLayout from "../components/GridLayout";
+import PageContainer from "../components/PageContainer";
+import Image from "../components/Image";
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+import { Core } from "@pano-to-mesh/three";
+import { encodeString } from "@pano-to-mesh/base64";
+
+const fetchAll = (callback) => {
+ fetch(process.env.NEXT_PUBLIC_DULA_NET_ADMIN_TASKS)
+ .then((data) => data.json())
+ .then((data) => callback(data.tasks))
+ .catch((e) => console.error(e));
+};
+
+const postImage = (formData, callback) => {
+ const api = process.env.NEXT_PUBLIC_DULA_NET_TASK;
+ const method = "POST";
+ const xhr = new XMLHttpRequest();
+ xhr.open(method, api, true);
+ xhr.onload = callback;
+ xhr.send(formData);
+};
+
+const getTask = (uuid, callback) => {
+ fetch(`${process.env.NEXT_PUBLIC_DULA_NET_TASK}?id=${uuid}`)
+ .then((data) => data.json())
+ .then((data) => {
+ if (data.output) {
+ const {
+ layout,
+ images: { aligned },
+ } = data.output;
+
+ const { cameraHeight, layoutHeight } = layout;
+
+ const point3dToCoords = (point) => {
+ const { longitude, latitude } =
+ Core.Math.coordinates.cartesian2Spherical(
+ point[0] - 0,
+ point[1] - cameraHeight,
+ point[2] - 0
+ );
+ const { x, y } = Core.Math.coordinates.spherical2NormalizedXY(
+ longitude,
+ latitude
+ );
+ return [1 - x, y];
+ };
+
+ const coords = layout.layoutPoints.points.map(({ xyz }) =>
+ point3dToCoords(xyz)
+ );
+
+ const viewer = {
+ ceilingY: layoutHeight,
+ floorY: 0,
+ layout2D: coords,
+ panorama: aligned,
+ panoramaOrigin: [0, cameraHeight, 0],
+ };
+
+ callback(viewer);
+ }
+ })
+ .catch((e) => console.error(e));
+};
+
+const Admin = () => {
+ const router = useRouter();
+ const [tasks, setTasks] = useState([]);
+ useEffect(() => {
+ fetchAll(setTasks);
+ }, []);
+
+ const onFileChange = (formData) => {
+ postImage(formData, () => {
+ fetchAll(setTasks);
+ });
+ };
+
+ const onClickFactory = (uuid) => {
+ return () => {
+ getTask(uuid, (data) => {
+ const hash = encodeString(JSON.stringify(data));
+ router.push(`/editors/layout2d#${hash}`);
+ });
+ };
+ };
+
+ return (
+
+
+
+ {tasks.map((data) => (
+
+ ))}
+
+
+ );
+};
+
+export default Admin;
diff --git a/components/GridLayout/index.js b/components/GridLayout/index.js
new file mode 100644
index 0000000..1188b69
--- /dev/null
+++ b/components/GridLayout/index.js
@@ -0,0 +1,7 @@
+import { Wrapper } from "./styled";
+
+const GridLayout = ({ children }) => {
+ return {children};
+};
+
+export default GridLayout;
diff --git a/components/GridLayout/styled.js b/components/GridLayout/styled.js
new file mode 100644
index 0000000..bf91f76
--- /dev/null
+++ b/components/GridLayout/styled.js
@@ -0,0 +1,12 @@
+import styled from "styled-components";
+
+export const Wrapper = styled.div`
+ display: inline-flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 10px;
+ padding: 10px;
+ width: 50%;
+ max-height: 80dvh;
+ overflow: auto;
+`;
diff --git a/components/Icon/index.js b/components/Icon/index.js
index 15909ac..15b78c9 100644
--- a/components/Icon/index.js
+++ b/components/Icon/index.js
@@ -38,6 +38,7 @@ const Icon = ({ src, onClick, ...props }) => {
// all svg resources is download from https://www.svgrepo.com/vectors/cursor/
const IconFolder = `/icons`;
const files = {
+ add: `${IconFolder}/add.svg`,
arrowToDown: `${IconFolder}/arrowToDown.svg`,
arrowToTop: `${IconFolder}/arrowToTop.svg`,
download: `${IconFolder}/download.svg`,
@@ -56,6 +57,8 @@ const files = {
trash: `${IconFolder}/trash.svg`,
arrange: `${IconFolder}/arrange.svg`,
preview: `${IconFolder}/preview.svg`,
+ github: `${IconFolder}/github-mark.svg`,
+ user: `${IconFolder}/user.svg`,
};
const Icons = Object.keys(files).reduce((acc, key) => {
diff --git a/components/Image/index.js b/components/Image/index.js
new file mode 100644
index 0000000..a1ca229
--- /dev/null
+++ b/components/Image/index.js
@@ -0,0 +1,13 @@
+import { Wrapper, Content, Container } from "./styled";
+
+const Image = ({ url, onClick }) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default Image;
diff --git a/components/Image/styled.js b/components/Image/styled.js
new file mode 100644
index 0000000..5a92fee
--- /dev/null
+++ b/components/Image/styled.js
@@ -0,0 +1,48 @@
+import styled from "styled-components";
+
+export const Wrapper = styled.div`
+ width: 95%;
+
+ // Small devices (landscape phones, 576px and up)
+ @media (min-width: 576px) {
+ width: 48%;
+ }
+
+ // Medium devices (tablets, 768px and up)
+ @media (min-width: 768px) {
+ width: 31%;
+ }
+
+ // Large devices (desktops, 992px and up)
+ @media (min-width: 992px) {
+ width: 23%;
+ }
+
+ // Extra large devices (large desktops, 1200px and up)
+ @media (min-width: 1200px) {
+ width: 18%;
+ }
+
+ &:hover {
+ cursor: pointer;
+ transform: scale(1.05);
+ }
+`;
+
+export const Container = styled.div`
+ position: relative;
+ width: 100%;
+ padding-top: 56%;
+`;
+
+export const Content = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-size: 100% 100%;
+ border-radius: 5px;
+
+ background-image: url("${(props) => props.src}");
+`;
diff --git a/components/RouteSwitch/index.js b/components/RouteSwitch/index.js
index 3ef7212..abdc27a 100644
--- a/components/RouteSwitch/index.js
+++ b/components/RouteSwitch/index.js
@@ -38,6 +38,14 @@ const DataWrapper = () => {
useEffect(() => {
setData([
+ {
+ link: "https://github.com/yushiang-demo/pano-to-mesh",
+ Icon: Icons.github,
+ },
+ {
+ link: "/admin",
+ Icon: Icons.user,
+ },
{
link: "/editors/layout2d",
Icon: Icons.panorama,
diff --git a/components/Upload/index.js b/components/Upload/index.js
new file mode 100644
index 0000000..0b7270e
--- /dev/null
+++ b/components/Upload/index.js
@@ -0,0 +1,31 @@
+import Icons from "../Icon";
+import { Input, Wrapper, Label } from "./styled";
+const Upload = ({ onFileChange }) => {
+ const onChange = (e) => {
+ const { files } = e.target;
+ const length = files.length;
+ Array.from({ length }).forEach((_, index) => {
+ const file = files[index];
+ const formData = new FormData();
+ formData.append("file", file);
+ onFileChange(formData);
+ });
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default Upload;
diff --git a/components/Upload/styled.js b/components/Upload/styled.js
new file mode 100644
index 0000000..5147058
--- /dev/null
+++ b/components/Upload/styled.js
@@ -0,0 +1,41 @@
+import styled from "styled-components";
+
+export const Input = styled.input`
+ display: none;
+`;
+
+export const Label = styled.label`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ cursor: pointer;
+ justify-content: center;
+ align-items: center;
+`;
+
+export const Wrapper = styled.div`
+ border: 1px solid;
+ border-radius: 5px;
+
+ width: 95%;
+
+ // Small devices (landscape phones, 576px and up)
+ @media (min-width: 576px) {
+ width: 48%;
+ }
+
+ // Medium devices (tablets, 768px and up)
+ @media (min-width: 768px) {
+ width: 31%;
+ }
+
+ // Large devices (desktops, 992px and up)
+ @media (min-width: 992px) {
+ width: 23%;
+ }
+
+ // Extra large devices (large desktops, 1200px and up)
+ @media (min-width: 1200px) {
+ width: 18%;
+ }
+`;
diff --git a/hooks/useClick2AddWalls.js b/hooks/useClick2AddWalls.js
index 3558854..3502a22 100644
--- a/hooks/useClick2AddWalls.js
+++ b/hooks/useClick2AddWalls.js
@@ -26,6 +26,7 @@ const useClick2AddWalls = ({
}) => {
const [dragging, setDragging] = useState(false);
const [previewImageCoord, setPreviewImageCoord] = useState(null);
+ const [previewCoordIndex, setPreviewCoordIndex] = useState(null);
const [imageCoord, setImageCoord] = useState(defaultData || []);
const [layout2D, setLayout2D] = useState([]);
@@ -67,48 +68,99 @@ const useClick2AddWalls = ({
setPreviewImageCoord([normalizedX, normalizedY]);
} else {
setPreviewImageCoord(null);
+ setPreviewCoordIndex(null);
}
};
const onMouseUp = ({ normalizedX, normalizedY }) => {
if (dragging) {
if (parseMousePointTo3D([normalizedX, normalizedY])) {
- setImageCoord([
- ...imageCoord,
- parser2DCeilingCoordToFloorCoord([normalizedX, normalizedY]),
- ]);
+ const newCoord = [...imageCoord];
+ newCoord.splice(
+ previewCoordIndex,
+ 0,
+ parser2DCeilingCoordToFloorCoord([normalizedX, normalizedY])
+ );
+
+ setImageCoord(newCoord);
}
setPreviewImageCoord(null);
+ setPreviewCoordIndex(null);
setDragging(false);
}
};
const onMouseDown = ({ width, height, normalizedX, normalizedY }) => {
- const points = imageCoord.filter(
- ([x, y]) =>
- !pointSelector(
- normalizedX,
- normalizedY,
- ({ x, y }) => ({
- x: x * width,
- y: y * height,
- }),
- selectThresholdPixel
- )([x, y])
+ const selectedIndex = imageCoord.findIndex(([x, y]) =>
+ pointSelector(
+ normalizedX,
+ normalizedY,
+ ({ x, y }) => ({
+ x: x * width,
+ y: y * height,
+ }),
+ selectThresholdPixel
+ )([x, y])
);
+ const points = imageCoord.filter((_, index) => index !== selectedIndex);
if (points.length) setImageCoord(points);
else
setImageCoord([
parser2DCeilingCoordToFloorCoord([normalizedX, normalizedY]),
]);
setDragging(true);
- setPreviewImageCoord([normalizedX, normalizedY]);
+ const targetCoord = [normalizedX, normalizedY];
+
+ const index = (() => {
+ if (selectedIndex > -1) return selectedIndex;
+ let index = null;
+ let boundaryIndex = null;
+ let targetWidth = 1; // max coord distance is 1;
+
+ const isEqual = (x, y) => x - y < 1e-3;
+
+ const xArray = imageCoord.map(([x]) => x);
+ const minX = Math.min(...xArray);
+ const maxX = Math.max(...xArray);
+
+ for (let i = 0; i < imageCoord.length; i++) {
+ const start = imageCoord[i][0];
+ const end = imageCoord[(i + 1) % imageCoord.length][0];
+
+ if (
+ ((isEqual(start, minX) && isEqual(end, maxX)) ||
+ (isEqual(end, minX) && isEqual(start, maxX))) &&
+ !boundaryIndex
+ ) {
+ boundaryIndex = i;
+ }
+
+ const newX = targetCoord[0];
+
+ const isInRange = isEqual(
+ Math.abs(newX - start) + Math.abs(newX - end) - Math.abs(start - end),
+ 0
+ );
+
+ const wallWidth = Math.abs(start - end);
+
+ if (isInRange && wallWidth < targetWidth) {
+ index = i;
+ targetWidth = wallWidth;
+ }
+ }
+
+ return (index !== null ? index : boundaryIndex) + 1;
+ })();
+ setPreviewImageCoord(targetCoord);
+ setPreviewCoordIndex(index);
};
useEffect(() => {
const coord2d = [...imageCoord];
- if (previewImageCoord) coord2d.push(previewImageCoord);
- coord2d.sort(([x1], [x2]) => x1 - x2);
+ if (previewImageCoord) {
+ coord2d.splice(previewCoordIndex, 0, previewImageCoord);
+ }
const pointsXZ = coord2d
.map((coord) => {
@@ -117,7 +169,7 @@ const useClick2AddWalls = ({
})
.filter((value) => value);
setLayout2D(pointsXZ);
- }, [imageCoord, previewImageCoord, parseMousePointTo3D]);
+ }, [imageCoord, previewImageCoord, previewCoordIndex, parseMousePointTo3D]);
return {
imageCoord,
diff --git a/pages/_app.js b/pages/_app.js
index d2bb9e8..a03d9d4 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -3,7 +3,7 @@ export default function App({ Component, pageProps }) {
return (
<>
- ;
+
>
);
}
diff --git a/pages/admin/index.js b/pages/admin/index.js
new file mode 100644
index 0000000..ca90fa1
--- /dev/null
+++ b/pages/admin/index.js
@@ -0,0 +1,3 @@
+import Admin from "../../apps/admin";
+
+export default Admin;
diff --git a/public/icons/add.svg b/public/icons/add.svg
new file mode 100644
index 0000000..3f32f53
--- /dev/null
+++ b/public/icons/add.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/public/icons/github-mark.svg b/public/icons/github-mark.svg
new file mode 100644
index 0000000..37fa923
--- /dev/null
+++ b/public/icons/github-mark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/user.svg b/public/icons/user.svg
new file mode 100644
index 0000000..4b30c94
--- /dev/null
+++ b/public/icons/user.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file