(layers);
16 | //FIXME: レイヤーをアップデートするときのみに使っていて、数字に意味はないので上の2つだけでうまくRedrawできないか?
17 | const [num, setNum] = useState(0);
18 |
19 | useEffect(() => {
20 | setLayerItems(layers);
21 | }, [layers]);
22 |
23 | const moveLayer = useCallback((dragIndex: number, hoverIndex: number) => {
24 | setLayerItems((prevLayers: Layer[]) =>
25 | update(prevLayers, {
26 | $splice: [
27 | [dragIndex, 1],
28 | [hoverIndex, 0, prevLayers[dragIndex] as Layer],
29 | ],
30 | }),
31 | );
32 | }, []);
33 |
34 | const renderLayer = useCallback(
35 | (layer: Layer, index: number) => {
36 | return ;
42 | }, [moveLayer]);
43 |
44 | return (
45 |
46 | {layerItems.map((layer, index) => renderLayer(layer, index))}
47 | }
52 | onClick={() => {
53 | const indexList = layers.map(layer => layer.index);
54 | indexList.push(layerIndex);
55 | layerIndex = Math.max(...indexList) + 1;
56 | layers.push(new Layer("Layer" + layerIndex, true, layerIndex, 3, false, Layer.IndexColor(layerIndex)));
57 | setLayers(layers);
58 | setLayerItems(layers);
59 | setNum(num + 1);
60 | }}
61 | />
62 | }
67 | onClick={() => {
68 | const res = confirm('delete-layer' + " \"" + floor + "\"");
69 | if (res) {
70 | setLayers(layers.filter(layer => layer.name !== floor));
71 | setNum(num - 1);
72 | }
73 | }}
74 | />
75 |
76 | );
77 | }
--------------------------------------------------------------------------------
/editor-ui/LayerItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useRef, useState } from "react";
2 | import { useDrag, useDrop } from "react-dnd";
3 | import { Identifier, XYCoord } from "dnd-core";
4 | import { Checkbox, Grid, TextField } from "@mui/material";
5 | import { Color, ColorValue, ColorPicker } from "mui-color";
6 | import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
7 | import LockIcon from '@mui/icons-material/Lock';
8 | import LockOpenIcon from '@mui/icons-material/LockOpen';
9 | import EditIcon from '@mui/icons-material/Edit';
10 | import EditOffIcon from '@mui/icons-material/EditOff';
11 | import { useEditPlansContext } from "./EditPlansContext";
12 | import VisibilityIcon from '@mui/icons-material/Visibility';
13 | import { Layer } from "../src/Canvas";
14 | import { B2DMath } from "../src/Utils";
15 |
16 | export interface LayerProps {
17 | index: number;
18 | layer: Layer;
19 | moveLayer: (dragIndex: number, hoverIndex: number) => void
20 | }
21 |
22 | interface DragItem {
23 | index: number
24 | id: string
25 | type: string
26 | }
27 |
28 | const style: React.CSSProperties | undefined = {
29 | padding: '2.5%',
30 | marginBottom: '.5rem',
31 | cursor: 'move',
32 | textAlign: 'center',
33 | borderRadius: "10px",
34 | verticalAlign: 'middle',
35 | backgroundColor: 'rgba(0,0,10,0.05)',
36 | };
37 |
38 | export const LayerItem: FC = ({ index, layer, moveLayer }) => {
39 | const ref = useRef(null);
40 | const { floor, setFloor, layers, setLayers } = useEditPlansContext();
41 |
42 | const [{ handlerId }, drop] = useDrop({
43 | accept: "Layer",
44 | collect(monitor) {
45 | return { handlerId: monitor.getHandlerId() };
46 | },
47 | hover(item: DragItem, monitor) {
48 | if (!ref.current) {
49 | return;
50 | }
51 | const dragIndex = item.index;
52 | const hoverIndex = index;
53 |
54 | if (dragIndex === hoverIndex) {
55 | return;
56 | }
57 |
58 | const hoverBoundingRect = ref.current?.getBoundingClientRect();
59 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
60 | const clientOffset = monitor.getClientOffset();
61 | const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
62 |
63 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
64 | return;
65 | }
66 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
67 | return;
68 | }
69 | moveLayer(dragIndex, hoverIndex);
70 | item.index = hoverIndex;
71 | },
72 | });
73 |
74 | const [{ isDragging }, drag] = useDrag({
75 | type: "Layer",
76 | item: () => {
77 | return { index };
78 | },
79 | collect: (monitor: any) => ({
80 | isDragging: monitor.isDragging(),
81 | }),
82 | });
83 |
84 | const opacity = isDragging ? 0.2 : 1;
85 | drag(drop(ref));
86 |
87 | // アクティブレイヤーの設定
88 | const activeChange = (event: React.ChangeEvent) => {
89 | if (event.target.checked) {
90 | setFloor(layer.name);
91 | layer.isVisible = true;
92 | layer.isLocked = false;
93 | }
94 | };
95 |
96 | // レイヤーカラーの設定
97 | const [, setColor] = useState();
98 | const colorChange = (color: Color) => {
99 | setColor(color);
100 | layer.color = "#" + color.hex;
101 | };
102 | layer.index = index;
103 |
104 | // レイヤーのロックの設定
105 | const [, setLock] = useState(layer.isLocked);
106 | const lockChange = (event: React.ChangeEvent) => {
107 | setLock(event.target.checked);
108 | layer.isLocked = event.target.checked;
109 | };
110 |
111 | // レイヤーの表示の設定
112 | const [, setVisible] = useState(layer.isVisible);
113 | const visibleChange = (event: React.ChangeEvent) => {
114 | setVisible(event.target.checked);
115 | layer.isVisible = event.target.checked;
116 | };
117 |
118 | // 階高の設定
119 | const [, setHeight] = useState(layer.height);
120 | const floorHeightChange = (event: React.ChangeEvent) => {
121 | layer.height = parseFloat(event.target.value);
122 | const index: number = layers.findIndex(l => l.name === layer.name);
123 | layers[index] = layer;
124 | setLayers(layers);
125 | setHeight(layer.height);
126 | };
127 |
128 | return (
129 |
130 |
131 |
132 | {(layer.index + 1) + "FL: " + layer.name}
133 |
134 |
135 |
136 |
137 |
138 | } checkedIcon={} checked={layer.name === floor} onChange={activeChange} />
139 |
140 |
141 | } checkedIcon={} checked={layer.isLocked} onChange={lockChange} />
142 |
143 |
144 | } checkedIcon={} checked={layer.isVisible} onChange={visibleChange} />
145 |
146 |
147 | void} hideTextfield />
148 |
149 |
150 |
151 | );
152 | };
153 |
--------------------------------------------------------------------------------
/editor-ui/NavSidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tabs from '@mui/material/Tabs';
3 | import { DndProvider } from 'react-dnd';
4 | import { HTML5Backend } from 'react-dnd-html5-backend';
5 | import { LayerContainer } from './LayerContainer';
6 | import dynamic from 'next/dynamic';
7 |
8 | const Sidebar = dynamic(() => import('./Sidebar'), {
9 | ssr: false,
10 | });
11 |
12 | export function DnDContent(): React.ReactElement {
13 | return (
14 | `calc(100vh - ${theme.mixins.toolbar.minHeight}px)`,
22 | borderRight: (theme) => `1px solid ${theme.palette.divider}`, backgroundColor: 'transparent', overflow: 'auto',
23 | }}
24 | >
25 |
26 |
27 | );
28 | }
29 |
30 | export default function NavSidebar(): React.ReactElement {
31 | return (
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/editor-ui/Sidebar/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTheme } from '@mui/material/styles';
3 | import Drawer, { DrawerProps } from '@mui/material/Drawer';
4 | import Toolbar from '@mui/material/Toolbar';
5 | import Box from '@mui/material/Box';
6 | import Fab from '@mui/material/Fab';
7 | import Dialog from '@mui/material/Dialog';
8 | import AppBar from '@mui/material/AppBar';
9 | import IconButton from '@mui/material/IconButton';
10 | import Typography from '@mui/material/Typography';
11 | import Slide from '@mui/material/Slide';
12 | import { TransitionProps } from '@mui/material/transitions';
13 | import CloseIcon from '@mui/icons-material/Close';
14 | import MenuOpenIcon from '@mui/icons-material/MenuOpen';
15 | import { useMobile } from './useWindowSize';
16 | import { SidebarProvider, useSidebarContext } from './SidebarContext';
17 |
18 | export const baseSidebarWidth = 240;
19 |
20 | const Transition = React.forwardRef(function Transition(props: TransitionProps & { children: React.ReactElement }, ref: React.Ref) {
21 | return ;
22 | });
23 |
24 | interface Props {
25 | children: React.ReactNode;
26 | title?: string;
27 | width?: number;
28 | anchor?: 'right' | 'left';
29 | swipeable?: boolean;
30 | drawerProps?: DrawerProps;
31 | }
32 |
33 | function SidebarContent({ children, title, width, anchor = 'right', swipeable = true, drawerProps }: Props): React.ReactElement {
34 | const theme = useTheme();
35 | const mobile = useMobile();
36 | const { open, handleOpen, handleClose } = useSidebarContext();
37 | const sidebarWidth = width || baseSidebarWidth;
38 |
39 | if (mobile && anchor === 'left') {
40 | return (
41 | <>
42 |
43 |
44 |
45 |
58 | >
59 | );
60 | }
61 |
62 | return (
63 |
79 |
80 | {children}
81 |
82 | );
83 | }
84 |
85 | export default function Sidebar(props: Props): React.ReactElement {
86 | return (
87 |
88 |
89 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/editor-ui/Sidebar/SidebarContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext, useMemo } from 'react';
2 |
3 | interface SidebarState {
4 | open: boolean,
5 | setOpen: (open: boolean) => void,
6 | handleOpen: () => void,
7 | handleClose: () => void,
8 | }
9 |
10 | const initialState: SidebarState = {
11 | open: false,
12 | setOpen: () => { return; },
13 | handleOpen: () => { return; },
14 | handleClose: () => { return; },
15 | };
16 |
17 | export const SidebarContext = React.createContext(initialState);
18 |
19 | interface SidebarProviderProps {
20 | children: React.ReactNode;
21 | }
22 |
23 | export function SidebarProvider({ children }: SidebarProviderProps): React.ReactElement {
24 | const [open, setOpen] = useState(initialState.open);
25 |
26 | const handleOpen = (): void => {
27 | setOpen(true);
28 | };
29 |
30 | const handleClose = (): void => {
31 | setOpen(false);
32 | };
33 |
34 | const sidebarState = useMemo((): SidebarState => {
35 | return {
36 | open,
37 | setOpen,
38 | handleOpen,
39 | handleClose,
40 | };
41 | }, [open]);
42 |
43 | return {children};
44 | }
45 |
46 | export function useSidebarContext(): SidebarState {
47 | return useContext(SidebarContext);
48 | }
49 |
--------------------------------------------------------------------------------
/editor-ui/Sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import Sidebar, { baseSidebarWidth } from './Sidebar';
2 | export default Sidebar;
3 |
4 | export { baseSidebarWidth };
5 |
6 | export { useSidebarContext } from './SidebarContext';
7 |
--------------------------------------------------------------------------------
/editor-ui/Sidebar/useWindowSize.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useCallback } from 'react';
2 | import { useTheme } from '@mui/material/styles';
3 | import useMediaQuery from '@mui/material/useMediaQuery';
4 |
5 | export function useEventListener(eventName: string, handler: (arg0: any) => void, element: Window | Document | HTMLCanvasElement = window): void {
6 | useEffect(() => {
7 | element.addEventListener(eventName, handler);
8 |
9 | return (): void => {
10 | element.removeEventListener(eventName, handler);
11 | };
12 | }, [element, eventName, handler]);
13 | }
14 |
15 | interface WindowSize {
16 | width: number;
17 | height: number;
18 | }
19 |
20 | export function useWindowSize(): WindowSize {
21 | const [windowSize, setWindowSize] = useState({ width: window?.innerWidth, height: window?.innerHeight });
22 |
23 | function onWindowResize(): void {
24 | setWindowSize({ width: window?.innerWidth, height: window?.innerHeight });
25 | }
26 |
27 | useEventListener('resize', onWindowResize);
28 |
29 | return windowSize;
30 | }
31 |
32 | export function useMobile(): boolean {
33 | const theme = useTheme();
34 | return useMediaQuery(theme.breakpoints.down('md'));
35 | }
36 |
37 | export function useLarge(): boolean {
38 | const theme = useTheme();
39 | return useMediaQuery(theme.breakpoints.up('lg'));
40 | }
41 |
42 | interface ParentSize {
43 | ref: (node: any) => void;
44 | width: number | undefined;
45 | height: number | undefined;
46 | }
47 |
48 | export function useParentSize(): ParentSize {
49 | const [height, setHeight] = useState(undefined);
50 | const [width, setWidth] = useState(undefined);
51 |
52 | const ref = useCallback((node: HTMLElement) => {
53 | if (node !== null) {
54 | setHeight(node.getBoundingClientRect().height);
55 | setWidth(node.getBoundingClientRect().width);
56 | }
57 | }, []);
58 |
59 | return {
60 | ref,
61 | width,
62 | height,
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/editor-ui/index.tsx:
--------------------------------------------------------------------------------
1 | import EditPlansCanvas from "./EditPlansCanvas";
2 | import { EditPlansProvider } from "./EditPlansContext";
3 | import InputSidebar from "./InputSidebar";
4 | import NavSidebar from "./NavSidebar";
5 |
6 | export default function EditPlans(): React.ReactElement {
7 | return (
8 | { e.preventDefault(); }}
10 | >
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "testMatch": [
3 | "**/__tests__/**/*.+(ts|tsx|js)",
4 | "**/?(*.)+(spec|test).+(ts|tsx|js)",
5 | ],
6 | "transform": {
7 | "^.+\\.(ts|tsx)$": "ts-jest",
8 | },
9 | };
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | basePath: process.env.GITHUB_ACTIONS && "/repository_name",
6 | trailingSlash: true,
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "building-editor-2d",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "tsc": "tsc --noEmit",
11 | "test": "jest",
12 | "cibuild": "next build && next export"
13 | },
14 | "dependencies": {
15 | "@auth0/nextjs-auth0": "^1.6.2",
16 | "@emotion/react": "^11.6.0",
17 | "@emotion/server": "^11.4.0",
18 | "@emotion/styled": "^11.6.0",
19 | "@mui/icons-material": "^5.0.4",
20 | "@mui/lab": "^5.0.0-alpha.55",
21 | "@mui/material": "^5.1.1",
22 | "@mui/styles": "^5.0.1",
23 | "@mui/x-data-grid": "^5.0.1",
24 | "@sentry/nextjs": "^7.2.0",
25 | "axios": "^0.24.0",
26 | "classnames": "^2.2.6",
27 | "date-fns": "^2.12.0",
28 | "form-data": "^4.0.0",
29 | "formidable": "v3",
30 | "github-slugger": "^1.3.0",
31 | "gray-matter": "^4.0.2",
32 | "immutability-helper": "^3.1.1",
33 | "lodash": "^4.17.21",
34 | "mathjs": "^9.4.4",
35 | "mdast-util-to-string": "^1.1.0",
36 | "mui-color": "^2.0.0-beta.2",
37 | "next": "^12.0.7",
38 | "next-pwa": "^5.4.1",
39 | "next-seo": "^4.26.0",
40 | "p5": "^1.4.0",
41 | "prop-types": "^15.7.2",
42 | "react": "^17.0.2",
43 | "react-datasheet": "^1.4.9",
44 | "react-dnd": "^16.0.1",
45 | "react-dnd-html5-backend": "^16.0.1",
46 | "react-dom": "^17.0.2",
47 | "react-google-charts": "^3.0.15",
48 | "react-hook-form": "^7.0.3",
49 | "react-markdown": "^5.0.3",
50 | "react-material-ui-carousel": "^3.1.0",
51 | "react-syntax-highlighter": "^15.4.3",
52 | "reconnecting-websocket": "^4.4.0",
53 | "remark": "^13.0.0",
54 | "remark-gfm": "^1.0.0",
55 | "sitemap": "^7.0.0",
56 | "swr": "^0.5.5",
57 | "three": "^0.128.0",
58 | "unist-util-visit": "^2.0.3"
59 | },
60 | "devDependencies": {
61 | "@types/formidable": "^1.2.4",
62 | "@types/github-slugger": "^1.3.0",
63 | "@types/jest": "^27.4.1",
64 | "@types/lodash": "^4.14.162",
65 | "@types/node": "^13.11.0",
66 | "@types/p5": "^1.3.0",
67 | "@types/react": "^17.0.14",
68 | "@types/react-syntax-highlighter": "^13.5.0",
69 | "@types/three": "^0.128.0",
70 | "@typescript-eslint/eslint-plugin": "^4.21.0",
71 | "@typescript-eslint/parser": "^4.21.0",
72 | "babel-plugin-transform-remove-console": "^6.9.4",
73 | "eslint": "^7.30.0",
74 | "eslint-config-next": "^12.0.4",
75 | "eslint-config-standard": "^14.1.1",
76 | "eslint-plugin-import": "^2.20.1",
77 | "eslint-plugin-node": "^11.0.0",
78 | "eslint-plugin-promise": "^4.2.1",
79 | "eslint-plugin-react": "^7.23.1",
80 | "eslint-plugin-react-hooks": "^4.2.0",
81 | "eslint-plugin-standard": "^4.0.1",
82 | "jest": "^27.5.1",
83 | "next-sitemap": "^1.6.95",
84 | "ts-jest": "^27.1.3",
85 | "typescript": "^4.5.5"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic';
2 | import Head from 'next/head';
3 | const Editor = dynamic(() => import('../editor-ui'), { ssr: false });
4 |
5 | export default function Home() {
6 | return (
7 |
8 |
9 |
BAUES Building Editor 2D
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/public/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baues/building-editor-2d/6d3dbb42173896079f4918d9d2f910395850323b/public/.nojekyll
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baues/building-editor-2d/6d3dbb42173896079f4918d9d2f910395850323b/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Canvas/CanvasDocument.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { CanvasObject } from './Object';
3 | import { Layer, GeometryObject, CanvasInfo } from '.';
4 | import { EditorState } from './EditorState';
5 | import { ObjectColor } from '../types';
6 |
7 | /**
8 | * キャンバスの情報を保持するクラス。
9 | * Rhino の実装を見ると 各 Geometry に対して cad のための付加的な情報を与えたものの集合。
10 | */
11 | export class CanvasDocument {
12 | /**レイヤー */
13 | layers: Layer[] = [];
14 | canvasInfo: CanvasInfo;
15 | /**スケールバーやオリエンテーションなどのキャンパス周りのオブジェクト */
16 | canvasObject: CanvasObject;
17 | /**ジオメトリのオブジェクト */
18 | geometryObjects: GeometryObject[] = [];
19 | editorState: EditorState = EditorState.defaultSettings();
20 |
21 | constructor(canvasInfo: CanvasInfo, canvasObject: CanvasObject, geometryObjects: GeometryObject[], layers: Layer[]) {
22 | this.canvasInfo = canvasInfo;
23 | this.canvasObject = canvasObject;
24 | this.geometryObjects = geometryObjects;
25 | if (layers.length === 0) {
26 | this.layers.push(new Layer("default", true, 0));
27 | } else {
28 | this.layers = layers;
29 | }
30 | }
31 |
32 | setVisibleLayers(visibleLayerNames: string[]): void {
33 | for (let i = 0; i < this.layers.length; i++) {
34 | this.layers[i].isVisible = visibleLayerNames.includes(this.layers[i].name) ? true : false;
35 | }
36 | }
37 |
38 | draw(p5: p5): void {
39 | const color = this.canvasInfo.colorSet.find(color => color.name === this.canvasInfo.colorMode)!;
40 | // canvasObject を描画
41 | this.canvasObject.draw(p5, color, this.canvasInfo.scale);
42 |
43 | const visibleLayers = this.layers.filter(layer => layer.isVisible).map(layer => layer.name);
44 | if (visibleLayers.length === 0) {
45 | return;
46 | }
47 |
48 | // 描画対象のレイヤーを描画
49 | // this.geometryObjects
50 | // .filter(obj => visibleLayers.includes(obj.layerName))
51 | // .forEach(obj => obj.draw(p5, color.default, this.canvasInfo.scale, false));
52 | for (let index = 0; index < this.geometryObjects.length; index++) {
53 | const obj = this.geometryObjects[index];
54 | if (visibleLayers.includes(obj.layerName)) {
55 | const drawColor: ObjectColor = {
56 | stroke: this.layers.find(layer => layer.name === obj.layerName)!.color,
57 | fill: color.default.fill,
58 | };
59 | obj.draw(p5, drawColor, this.canvasInfo.scale, false);
60 | }
61 | }
62 |
63 | // 選択されているオブジェクトを描画
64 | this.geometryObjects
65 | .filter(obj => obj.isSelected)
66 | .forEach(obj => obj.draw(p5, color.select, this.canvasInfo.scale, true));
67 |
68 | // 一時的なジオメトリの描画
69 | this.editorState.editingGeometry
70 | .filter(obj => obj.isVisible)
71 | .forEach(obj => obj.draw(p5, color.default, this.canvasInfo.scale, true));
72 | }
73 |
74 | clearTempGeometry(): void {
75 | this.editorState.editingGeometry = [];
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Canvas/CanvasInfo.ts:
--------------------------------------------------------------------------------
1 | import { Point, Vector } from '../Geometry';
2 | import { ObjectColorSet } from '../types';
3 |
4 | export class CanvasInfo {
5 | scale: number;
6 | height: number;
7 | width: number;
8 | drawCenter: Point;
9 | colorSet: ObjectColorSet[];
10 | colorMode = "";
11 | /**作図対象になっているレイヤー名。可視化するかどうかはレイヤーの isVisible が持っている*/
12 | activeLayer = "Layer0";
13 | /**Canvas で扱うモデルで保証する精度の小数点以下桁数。 0 ならば整数、1 ならば少数第一位まで*/
14 | tolerance: number;
15 | /**扱うモデルの桁数*/
16 | toleranceAngle: number;
17 | /**扱うモデルの桁数*/
18 | gridInterval: number;
19 | /***/
20 | isActive = true;
21 | /**単位*/
22 | unit = {
23 | length: "m",
24 | angle: "radian",
25 | };
26 |
27 | constructor(
28 | scale: number, height: number, width: number,
29 | drawCenter = Point.zero(), colorSet = CanvasInfo.defaultColor(),
30 | tolerance = 4, toleranceAngle = 1, gridInterval = 1,
31 | ) {
32 | this.scale = scale;
33 | this.height = height;
34 | this.width = width;
35 | this.drawCenter = drawCenter;
36 | this.colorSet = colorSet;
37 | this.tolerance = tolerance;
38 | this.toleranceAngle = toleranceAngle;
39 | this.gridInterval = gridInterval;
40 | }
41 |
42 | /**
43 | * 桁数ではなく数値での tolerance を返す
44 | * @returns
45 | */
46 | toleranceValue(): number {
47 | return Math.pow(10, -this.tolerance);
48 | }
49 |
50 | canvasTranslate(): Vector {
51 | return new Vector(
52 | this.width / 2 - this.drawCenter.x * this.scale,
53 | this.height / 2 - this.drawCenter.y * this.scale,
54 | );
55 | }
56 |
57 | addColor(color: ObjectColorSet): void {
58 | this.colorSet.push(color);
59 | }
60 |
61 | deleteColor(name: string): void {
62 | this.colorSet = this.colorSet.filter(color => color.name !== name);
63 | }
64 |
65 | static defaultColor(): ObjectColorSet[] {
66 | const colors: ObjectColorSet[] = [];
67 | colors.push({
68 | name: 'light',
69 | default: {
70 | stroke: 'rgba(1, 1, 1, 1)',
71 | fill: 'rgba(1, 1, 1 ,1)',
72 | },
73 | select: {
74 | stroke: 'rgba(255, 0, 0, 1)',
75 | fill: 'rgba(240, 128, 128, 0.5)',
76 | },
77 | axis: {
78 | stroke: 'blue',
79 | fill: 'red',
80 | },
81 | grid: {
82 | stroke: 'lightgray',
83 | fill: 'lightgray',
84 | },
85 | scaleBar: {
86 | stroke: 'black',
87 | fill: 'black',
88 | },
89 | orientation: {
90 | stroke: 'black',
91 | fill: 'black',
92 | },
93 | });
94 | colors.push({
95 | name: 'dark',
96 | default: {
97 | stroke: 'white',
98 | fill: 'white',
99 | },
100 | select: {
101 | stroke: 'rgba(255, 0, 0, 1)',
102 | fill: 'rgba(240, 128, 128, 0.5)',
103 | },
104 | axis: {
105 | stroke: 'blue',
106 | fill: 'red',
107 | },
108 | grid: {
109 | stroke: '#444444',
110 | fill: '#444444',
111 | },
112 | scaleBar: {
113 | stroke: 'white',
114 | fill: 'white',
115 | },
116 | orientation: {
117 | stroke: 'white',
118 | fill: 'white',
119 | },
120 | });
121 |
122 | return colors;
123 | }
124 |
125 | setDefaultColor(): void {
126 | this.colorSet = CanvasInfo.defaultColor();
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Canvas/EditorState.ts:
--------------------------------------------------------------------------------
1 | import { Snap } from '../types';
2 | import { GeometryObject } from '.';
3 |
4 | export class EditorState {
5 | snap: Snap;
6 | function: string;
7 | editingGeometry: GeometryObject[] = [];
8 |
9 | constructor(editFunction: string, snap: Snap) {
10 | this.function = editFunction;
11 | this.snap = snap;
12 | }
13 |
14 | static defaultSettings(): EditorState {
15 | const editMode = "Select";
16 | const snap = {
17 | mode: {
18 | endPoint: true,
19 | middle: false,
20 | near: false,
21 | angle: false,
22 | grid: false,
23 | },
24 | point: null,
25 | holdPoint: null,
26 | objectIndex: null,
27 | };
28 |
29 | return new EditorState(editMode, snap);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Canvas/Interface.ts:
--------------------------------------------------------------------------------
1 | import { Point, Vector } from '../Geometry';
2 | import { ObjectColor } from '../types';
3 | import p5 from 'p5';
4 |
5 | /**
6 | * 描画可能オブジェクトのインターフェース
7 | */
8 | export interface DrawableObject {
9 | isVisible: boolean;
10 | draw(p5: p5, objectColor: ObjectColor, scale: number, isFill: boolean): void;
11 | }
12 |
13 | /**
14 | * ポイントのスナップに関するのインターフェース
15 | */
16 | export interface PointSnap {
17 | snapNear(p5: p5, pan: Vector, distance: number): Point | null;
18 | }
19 |
20 | /**
21 | * カーブのスナップに関するのインターフェース
22 | */
23 | export interface CurveSnap extends PointSnap {
24 | snapNear(p5: p5, pan: Vector, distance: number): Point | null;
25 | snapMiddle(p5: p5, pan: Vector, distance: number): Point | null;
26 | snapEndPoint(p5: p5, pan: Vector, distance: number): Point | null;
27 | }
28 |
--------------------------------------------------------------------------------
/src/Canvas/Layer.ts:
--------------------------------------------------------------------------------
1 | const colorList = ["red", "green", "blue", "yellow", "orange", "purple", "pink", "brown", "black"];
2 |
3 | export class Layer {
4 | name: string
5 | color: string;
6 | index: number;
7 | height: number;
8 | isVisible = true;
9 | isLocked = false;
10 |
11 | constructor(name: string, isVisible: boolean, index: number, height = 3000, isLocked = false, color = '#000000') {
12 | this.name = name;
13 | this.isVisible = isVisible;
14 | this.index = index;
15 | this.isLocked = isLocked;
16 | this.height = height;
17 |
18 | const numColor = Number(color);
19 | if (isNaN(numColor) || numColor > colorList.length) {
20 | this.color = color;
21 | } else {
22 | this.color = colorList[numColor];
23 | }
24 | }
25 |
26 | static IndexColor(index: number): string {
27 | if (index >= colorList.length) {
28 | return colorList[index % colorList.length];
29 | }
30 | return colorList[index];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Canvas/Object/AxisObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { Point } from '../../Geometry';
3 | import { DrawableObject } from "../../Canvas";
4 | import { ObjectColor } from '../../types';
5 |
6 | export class AxisObject implements DrawableObject {
7 | isVisible = true;
8 | origin: Point;
9 | length: number;
10 |
11 | constructor(origin = Point.zero(), length = 100000) {
12 | this.origin = origin;
13 | this.length = length;
14 | }
15 |
16 | /**
17 | * 軸の描画
18 | * @param p5
19 | * @param color fill が X軸、stroke が Y軸 のカラーになる
20 | * @param scale
21 | */
22 | draw(p5: p5, color: ObjectColor, scale: number): void {
23 | const origin = new Point(this.origin.x, this.origin.y);
24 | const length = this.length;
25 |
26 | p5.strokeWeight(2.0 / scale);
27 | p5.stroke(color.fill);
28 | p5.line(origin.x, origin.y, origin.x + length, origin.y);
29 | p5.stroke(color.stroke);
30 | p5.line(origin.x, origin.y, -origin.x, -(origin.y + length));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Canvas/Object/CanvasObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { AxisObject, GridObject, OrientationObject, ScaleObject } from '../../Canvas/Object';
3 | import { ObjectColorSet } from '../../types';
4 |
5 | export class CanvasObject {
6 | isVisible = true;
7 | grid: GridObject;
8 | axis: AxisObject;
9 | scaleBar: ScaleObject;
10 | orientation: OrientationObject;
11 |
12 | constructor(grid = new GridObject(), axis = new AxisObject(), scaleBar = new ScaleObject(), orientation = new OrientationObject()) {
13 | this.grid = grid;
14 | this.axis = axis;
15 | this.scaleBar = scaleBar;
16 | this.orientation = orientation;
17 | }
18 |
19 | draw(p5: p5, color: ObjectColorSet, scale: number): void {
20 | if (this.isVisible) {
21 | if (this.grid.isVisible) {
22 | this.grid.draw(p5, color.grid, scale);
23 | }
24 | if (this.axis.isVisible) {
25 | this.axis.draw(p5, color.axis, scale);
26 | }
27 | if (this.scaleBar.isVisible) {
28 | this.scaleBar.draw(p5, color.scaleBar, scale);
29 | }
30 | if (this.orientation.isVisible) {
31 | this.orientation.draw(p5, color.orientation, scale);
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Canvas/Object/GeometryObject.ts:
--------------------------------------------------------------------------------
1 | import { Vector, GeometryBase, Point } from '../../Geometry';
2 | import { ObjectColor, SnapMode } from '../../types';
3 | import p5 from 'p5';
4 | import { DrawableObject } from '../Interface';
5 |
6 | /**
7 | * ジオメトリオブジェクトのインターフェース
8 | */
9 | export interface GeometryObject extends DrawableObject {
10 | /**オブジェクトが保持するジオメトリ情報 */
11 | geometry: GeometryBase;
12 | /**オブジェクトの名前 */
13 | name: string;
14 | /**オブジェクトの所属するレイヤー名 */
15 | objectType: string;
16 | /**オブジェクトのタイプ情報 */
17 | layerName: string;
18 | /**オブジェクト単体に対する可視化のブール値。これとは別に所属するレイヤーに対しても可視化のブール値がある */
19 | isVisible: boolean;
20 | /**オブジェクトが選択されているかのブール値 */
21 | isSelected: boolean;
22 |
23 | draw(p5: p5, objectColor: ObjectColor, scale: number, isFill: boolean): void;
24 | snap(p5: p5, snapMode: SnapMode, pan: Vector, scale: number): Point | null;
25 | mouseDist(p5: p5, pan: Vector, scale: number): number;
26 | }
27 |
--------------------------------------------------------------------------------
/src/Canvas/Object/GridObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { Point } from '../../Geometry';
3 | import { DrawableObject } from "../../Canvas";
4 | import { ObjectColor } from '../../types';
5 |
6 | export class GridObject implements DrawableObject {
7 | isVisible = true;
8 | size: number;
9 | primarySpacing: number;
10 | subdivisions: number;
11 | origin: Point;
12 |
13 | constructor(size = 1000, primarySpacing = 10, subdivisions = 5, origin = Point.zero()) {
14 | this.size = size;
15 | this.primarySpacing = primarySpacing;
16 | this.subdivisions = subdivisions;
17 | this.origin = origin;
18 | }
19 |
20 | /**
21 | *
22 | * @param p5
23 | * @param color fill が内部分割の線の色、stroke がメインのグリッドの色
24 | * @param scale
25 | */
26 | draw(p5: p5, color: ObjectColor, scale: number): void {
27 | const length = this.size / 2;
28 | const origin = new Point(this.origin.x, this.origin.y);
29 |
30 | for (let i = 0; i < length / this.primarySpacing; i++) {
31 | p5.stroke(color.fill);
32 | p5.strokeWeight(1.0 / scale / 2);
33 | const interval = i * this.primarySpacing;
34 |
35 | for (let j = 1; j < this.subdivisions; j++) {
36 | const subInterval = j * this.primarySpacing / this.subdivisions + interval;
37 | p5.line(origin.x + subInterval, origin.y - length, origin.x + subInterval, origin.y + length);
38 | p5.line(origin.x - length, origin.y + subInterval, origin.x + length, origin.y + subInterval);
39 | p5.line(origin.x - subInterval, origin.y - length, origin.x - subInterval, origin.y + length);
40 | p5.line(origin.x - length, origin.y - subInterval, origin.x + length, origin.y - subInterval);
41 | }
42 |
43 | p5.stroke(color.stroke);
44 | p5.strokeWeight(1.0 / scale);
45 | p5.line(origin.x + interval, origin.y - length, origin.x + interval, origin.y + length);
46 | p5.line(origin.x - length, origin.y + interval, origin.x + length, origin.y + interval);
47 | p5.line(origin.x - interval, origin.y - length, origin.x - interval, origin.y + length);
48 | p5.line(origin.x - length, origin.y - interval, origin.x + length, origin.y - interval);
49 | }
50 |
51 | p5.line(origin.x + length, origin.y - length, origin.x + length, origin.y + length);
52 | p5.line(origin.x - length, origin.y + length, origin.x + length, origin.y + length);
53 | p5.line(origin.x - length, origin.y - length, origin.x - length, origin.y + length);
54 | p5.line(origin.x - length, origin.y - length, origin.x + length, origin.y - length);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Canvas/Object/LineObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { Vector, Point, Line } from '../../Geometry';
3 | import { CurveSnap } from "../Interface";
4 | import { GeometryObject } from "./GeometryObject";
5 | import { ObjectColor, SnapMode } from '../../types';
6 | import { PointObject } from './PointObject';
7 |
8 | export class LineObject implements GeometryObject, CurveSnap {
9 | /**オブジェクトが保持するジオメトリ情報 */
10 | geometry: Line;
11 | /**オブジェクトの名前 */
12 | name: string;
13 | /**オブジェクトの所属するレイヤー名 */
14 | layerName: string;
15 | /**オブジェクトのタイプ情報 */
16 | objectType = "line";
17 | /**オブジェクト単体に対する可視化のブール値。これとは別に所属するレイヤーに対しても可視化のブール値がある */
18 | isVisible = true;
19 | /**オブジェクトが選択されているかのブール値 */
20 | isSelected = false;
21 |
22 | constructor(geometry: Line, name = "", layerName = "default") {
23 | this.geometry = geometry;
24 | this.name = name;
25 | this.layerName = layerName;
26 | }
27 |
28 | draw(p5: p5, color: ObjectColor, scale: number, isFill = false, weight = 1): void {
29 | const line = this.geometry;
30 | p5.stroke(color.stroke);
31 | p5.strokeWeight(weight / scale);
32 | p5.line(line.from.x, line.from.y, line.to.x, line.to.y);
33 | }
34 |
35 | /**
36 | * マウスのポイントと Line との距離を求める。
37 | * @param p5
38 | * @param pan
39 | * @param scale
40 | * @returns
41 | */
42 | mouseDist(p5: p5, pan: Vector, scale: number): number {
43 | const mousePt: Point = PointObject.mousePt(p5, pan, scale).geometry;
44 | return this.geometry.distance(mousePt);
45 | }
46 |
47 | mouseClosestPoint(p5: p5, pan: Vector, scale: number): Point {
48 | const mousePt: Point = PointObject.mousePt(p5, pan, scale).geometry;
49 | return this.geometry.closestPoint(mousePt);
50 | }
51 |
52 | snap(p5: p5, snapMode: SnapMode, pan: Vector, scale: number): Point | null {
53 | let pt: Point | null = null;
54 | if (snapMode.near) {
55 | const snapPt = this.snapNear(p5, pan, scale);
56 | pt = snapPt ? snapPt : pt;
57 | }
58 | if (snapMode.endPoint) {
59 | const snapPt = this.snapEndPoint(p5, pan, scale);
60 | pt = snapPt ? snapPt : pt;
61 | }
62 | if (snapMode.middle) {
63 | const snapPt = this.snapMiddle(p5, pan, scale);
64 | pt = snapPt ? snapPt : pt;
65 | }
66 | return pt;
67 | }
68 |
69 | snapNear(p5: p5, pan: Vector, scale: number, distance = 50): Point | null {
70 | const line = this.geometry;
71 | const mousePt = PointObject.mousePt(p5, pan, scale).geometry;
72 |
73 | if (line.distance(mousePt) < distance / scale){
74 | return line.closestPoint(mousePt);
75 | } else {
76 | return null;
77 | }
78 | }
79 |
80 | snapMiddle(p5: p5, pan: Vector, scale: number, distance = 30): Point | null {
81 | const line = this.geometry;
82 | const mousePt = PointObject.mousePt(p5, pan, scale).geometry;
83 | const center = line.middle();
84 | const dist = mousePt.distance(center);
85 |
86 | if (dist < distance / scale) {
87 | return center;
88 | } else {
89 | return null;
90 | }
91 | }
92 |
93 | snapEndPoint(p5: p5, pan: Vector, scale: number, distance = 30): Point | null {
94 | const line = this.geometry;
95 | const mousePt = PointObject.mousePt(p5, pan, scale).geometry;
96 | const fromDist = mousePt.distance(line.from);
97 | const toDist = mousePt.distance(line.to);
98 |
99 | const minDist = Math.min(fromDist, toDist);
100 | const minPt = fromDist < toDist ? line.from : line.to;
101 |
102 | if (minDist < distance / scale) {
103 | return minPt;
104 | } else {
105 | return null;
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/Canvas/Object/OrientationObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { DrawableObject } from '../../Canvas';
3 | import { ObjectColor } from '../../types';
4 | import { Point } from '../../Geometry';
5 |
6 | export class OrientationObject implements DrawableObject {
7 | isVisible = true;
8 | northAngle: number;
9 | drawCenter: Point;
10 |
11 | constructor(northAngle = 0, drawCenter = new Point(0, 0)) {
12 | this.northAngle = northAngle;
13 | this.drawCenter = drawCenter;
14 | }
15 |
16 | draw(p5: p5, color: ObjectColor, scale: number): void {
17 | const size = 20 / scale;
18 | const margin = 20 / scale;
19 | const angle = this.northAngle / 180 * Math.PI;
20 | const width = p5.windowWidth / scale;
21 | const height = p5.windowHeight / scale;
22 | const center = new Point(
23 | (size + margin) - width / 2 + this.drawCenter.x,
24 | height / 2 - (size + 4 * margin) + this.drawCenter.y,
25 | );
26 |
27 | p5.push();
28 | p5.noFill();
29 | p5.stroke(color.stroke);
30 | p5.strokeWeight(1 / scale);
31 | p5.circle(center.x, center.y, size * 2);
32 | this.triangle(p5, center.x, center.y, angle, size);
33 |
34 | p5.fill(color.fill);
35 | const letter = p5.char(78);
36 | const r2 = size + margin / 2;
37 | p5.textSize(20 / scale);
38 | p5.text(letter, center.x + r2 * Math.sin(angle), center.y - r2 * Math.cos(angle));
39 | p5.pop();
40 | }
41 |
42 | private triangle(p: p5, x: number, y: number, angle: number, r: number): void {
43 | // this code is to make the arrow point
44 | p.push(); // start new drawing state
45 | p.translate(x, y); // translates to the destination vertex
46 | p.rotate(angle); // rotates the arrow point
47 | p.triangle(-r / 2 * Math.sqrt(3), r / 2, r / 2 * Math.sqrt(3), r / 2, 0, -r); // draws the arrow point as a triangle
48 | p.pop();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Canvas/Object/PointObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { Point, Vector } from "../../Geometry";
3 | import { GeometryObject } from "./GeometryObject";
4 | import { ObjectColor, SnapMode } from '../../types';
5 |
6 | export class PointObject implements GeometryObject {
7 | /**オブジェクトが保持するジオメトリ情報 */
8 | geometry: Point;
9 | /**オブジェクトの名前 */
10 | name: string;
11 | /**オブジェクトの所属するレイヤー名 */
12 | layerName: string;
13 | /**オブジェクトのタイプ情報 */
14 | objectType = "Point";
15 | /**オブジェクト単体に対する可視化のブール値。これとは別に所属するレイヤーに対しても可視化のブール値がある */
16 | isVisible = true;
17 | /**オブジェクトが選択されているかのブール値 */
18 | isSelected = false;
19 |
20 | constructor(geometry: Point, name = "", layerName = "default") {
21 | this.geometry = geometry;
22 | this.name = name;
23 | this.layerName = layerName;
24 | }
25 |
26 | snap(p5: p5, snapMode: SnapMode, pan: Vector, scale: number): Point | null {
27 | if (snapMode.near || snapMode.endPoint || snapMode.middle) {
28 | return this.snapNear(p5, pan, scale);
29 | } else {
30 | return null;
31 | }
32 | }
33 |
34 | snapNear(p5: p5, pan: Vector, scale: number, distance = 30): Point | null {
35 | const dist = this.mouseDist(p5, pan, scale);
36 | if (dist < distance / scale) {
37 | return this.geometry;
38 | } else {
39 | return null;
40 | }
41 | }
42 |
43 | static mousePt(p5: p5, pan: Vector, scale: number, layerName = "default"): PointObject {
44 | return new PointObject(new Point(p5.mouseX - pan.x, p5.mouseY - pan.y).divide(scale), "", layerName);
45 | }
46 |
47 | draw(p5: p5, color: ObjectColor, scale: number, isFill = false, diameter = 1, weight = 1): void {
48 | const pt = this.geometry;
49 | if (isFill) {
50 | p5.fill(color.fill);
51 | } else {
52 | p5.noFill();
53 | }
54 | p5.stroke(color.stroke);
55 | p5.strokeWeight(weight / scale);
56 | p5.circle(pt.x, pt.y, diameter / scale);
57 | }
58 |
59 | mouseDist(p5: p5, pan: Vector, scale: number): number {
60 | const pt = this.geometry;
61 | const mousePt = PointObject.mousePt(p5, pan, scale).geometry;
62 | return Math.sqrt((pt.x - mousePt.x) ** 2 + (pt.y - mousePt.y) ** 2);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Canvas/Object/PolylineObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { CurveSnap } from "../Interface";
3 | import { GeometryObject } from "./GeometryObject";
4 | import { Point, Polyline, Vector } from '../../Geometry';
5 | import { LineObject } from '../../Canvas/Object';
6 | import { ObjectColor, SnapMode } from '../../types';
7 | import { PointObject } from './PointObject';
8 |
9 | /**
10 | * 2D のポリラインオブジェクトを表すクラス
11 | */
12 | export class PolylineObject implements GeometryObject, CurveSnap {
13 | /**オブジェクトが保持するジオメトリ情報 */
14 | geometry: Polyline;
15 | /**オブジェクトの名前 */
16 | name: string;
17 | /**オブジェクトの所属するレイヤー名 */
18 | layerName: string;
19 | /**オブジェクトのタイプ情報 */
20 | objectType = "Polyline";
21 | /**オブジェクト単体に対する可視化のブール値。これとは別に所属するレイヤーに対しても可視化のブール値がある */
22 | isVisible = true;
23 | /**オブジェクトが選択されているかのブール値 */
24 | isSelected = false;
25 |
26 | constructor(geometry: Polyline, name = "", layerName = "default") {
27 | this.geometry = geometry;
28 | this.name = name;
29 | this.layerName = layerName;
30 | }
31 |
32 | toLineObjects(): LineObject[] {
33 | return this.geometry.toLineArray().map(line => new LineObject(line));
34 | }
35 |
36 | toPointObjects(): PointObject[] {
37 | return this.geometry.ptList.map(pt => new PointObject(pt));
38 | }
39 |
40 | draw(p5: p5, color: ObjectColor, scale: number, isFill = false): void {
41 | const ptList = this.geometry.toPointArray();
42 |
43 | p5.stroke(color.stroke);
44 | p5.strokeWeight(2.0 / scale);
45 | if (isFill) {
46 | p5.fill(color.fill);
47 | p5.beginShape();
48 | for (const pt of ptList) {
49 | p5.vertex(pt.x, pt.y);
50 | }
51 | if (this.geometry.isClosed) {
52 | p5.vertex(ptList[0].x, ptList[0].y);
53 | }
54 | p5.endShape(p5.CLOSE);
55 | } else {
56 | for (let i = 0; i < ptList.length - 1; i++) {
57 | p5.line(
58 | ptList[i].x, ptList[i].y,
59 | ptList[i + 1].x, ptList[i + 1].y,
60 | );
61 | }
62 | if (this.geometry.isClosed) {
63 | p5.line(
64 | ptList[ptList.length - 1].x, ptList[ptList.length - 1].y,
65 | ptList[0].x, ptList[0].y,
66 | );
67 | }
68 | }
69 | }
70 |
71 | mouseDist(p5: p5, pan = Vector.zero(), scale: number): number {
72 | let dist: number = Number.MAX_VALUE;
73 |
74 | this.toLineObjects().forEach((lineObj) => {
75 | const d = lineObj.mouseDist(p5, pan, scale);
76 | if (d < dist) {
77 | dist = d;
78 | }
79 | });
80 |
81 | if (dist === Number.MAX_VALUE){
82 | return Number.NaN;
83 | } else {
84 | return dist;
85 | }
86 | }
87 |
88 | snap(p5: p5, snapMode: SnapMode, pan: Vector, scale: number): Point | null {
89 | let pt: Point | null = null;
90 | if (snapMode.near) {
91 | const snapPt = this.snapNear(p5, pan, scale);
92 | pt = snapPt ? snapPt : pt;
93 | }
94 | if (snapMode.endPoint) {
95 | const snapPt = this.snapEndPoint(p5, pan, scale);
96 | pt = snapPt ? snapPt : pt;
97 | }
98 | if (snapMode.middle) {
99 | const snapPt = this.snapMiddle(p5, pan, scale);
100 | pt = snapPt ? snapPt : pt;
101 | }
102 | return pt;
103 | }
104 |
105 | snapNear(p5: p5, pan: Vector, scale: number, distance = 50): Point | null {
106 | const snapPts = this.toLineObjects().map((lineObj) => {
107 | return lineObj.snapNear(p5, pan, scale, distance);
108 | });
109 | const snapDist = snapPts.map((pt) => {
110 | return pt?.distance(PointObject.mousePt(p5, pan, scale).geometry);
111 | });
112 |
113 | const index = this.getSnapPtIndex(snapDist);
114 | if (index >= 0) {
115 | return snapPts[index];
116 | } else {
117 | return null;
118 | }
119 | }
120 |
121 | snapMiddle(p5: p5, pan: Vector, scale: number, distance = 30): Point | null {
122 | const snapPts = this.toLineObjects().map((lineObj) => {
123 | return new PointObject(lineObj.geometry.middle()).snapNear(p5, pan, scale, distance);
124 | });
125 | const snapDist = snapPts.map((pt) => {
126 | return pt?.distance(PointObject.mousePt(p5, pan, scale).geometry);
127 | });
128 |
129 | const index = this.getSnapPtIndex(snapDist);
130 | if (index >= 0) {
131 | return snapPts[index];
132 | } else {
133 | return null;
134 | }
135 | }
136 |
137 | snapEndPoint(p5: p5, pan: Vector, scale: number, distance = 30): Point | null {
138 | const snapPts = this.toPointObjects().map((ptObj) => {
139 | return ptObj.snapNear(p5, pan, scale, distance);
140 | });
141 | const snapDist = snapPts.map((pt) => {
142 | return pt?.distance(PointObject.mousePt(p5, pan, scale).geometry);
143 | });
144 |
145 | const index = this.getSnapPtIndex(snapDist);
146 | if (index >= 0) {
147 | return snapPts[index];
148 | } else {
149 | return null;
150 | }
151 | }
152 |
153 | private getSnapPtIndex(snapDist: (number | undefined)[]): number {
154 | let minDist = Number.MAX_VALUE;
155 | for (let i = 0; i < snapDist.length; i++) {
156 | const element: number = snapDist[i]!;
157 | if (element >= 0) {
158 | minDist = element < minDist ? element : minDist;
159 | }
160 | }
161 |
162 | const index = snapDist.indexOf(minDist);
163 | if (index >= 0) {
164 | return index;
165 | } else {
166 | return -1;
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/Canvas/Object/RectangleObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { Vector, Rectangle, Point } from '../../Geometry';
3 | import { CurveSnap } from '../Interface';
4 | import { GeometryObject } from "./GeometryObject";
5 | import { LineObject, PolylineObject } from '.';
6 | import { ObjectColor, SnapMode } from '../../types';
7 |
8 | //TODO: polyline と共通化する(CurveObject?を作る)
9 | export class RectangleObject implements GeometryObject, CurveSnap {
10 | /**オブジェクトが保持するジオメトリ情報 */
11 | geometry: Rectangle;
12 | /**オブジェクトの名前 */
13 | name: string;
14 | /**オブジェクトの所属するレイヤー名 */
15 | layerName: string;
16 | /**オブジェクトのタイプ情報 */
17 | objectType = "Rectangle";
18 | /**オブジェクト単体に対する可視化のブール値。これとは別に所属するレイヤーに対しても可視化のブール値がある */
19 | isVisible = true;
20 | /**オブジェクトが選択されているかのブール値 */
21 | isSelected = false;
22 |
23 | constructor(geometry: Rectangle, name = "", layerName = "default") {
24 | this.geometry = geometry;
25 | this.name = name;
26 | this.layerName = layerName;
27 | }
28 |
29 | toLineObjects(): LineObject[] {
30 | return this.geometry.toLineArray().map(line => new LineObject(line));
31 | }
32 |
33 | toPolylineObject(): PolylineObject {
34 | return new PolylineObject(this.geometry.toPolyline(), this.name, this.layerName);
35 | }
36 |
37 | draw(p5: p5, color: ObjectColor, scale: number, isFill = false): void {
38 | this.toPolylineObject().draw(p5, color, scale, isFill);
39 | }
40 |
41 | mouseDist(p5: p5, pan: Vector, scale: number): number {
42 | return this.toPolylineObject().mouseDist(p5, pan, scale);
43 | }
44 |
45 | snap(p5: p5, snapMode: SnapMode, pan: Vector, scale: number): Point | null {
46 | let pt: Point | null = null;
47 | if (snapMode.near) {
48 | const snapPt = this.snapNear(p5, pan, scale);
49 | pt = snapPt ? snapPt : pt;
50 | }
51 | if (snapMode.endPoint) {
52 | const snapPt = this.snapEndPoint(p5, pan, scale);
53 | pt = snapPt ? snapPt : pt;
54 | }
55 | if (snapMode.middle) {
56 | const snapPt = this.snapMiddle(p5, pan, scale);
57 | pt = snapPt ? snapPt : pt;
58 | }
59 | return pt;
60 | }
61 |
62 | snapNear(p5: p5, pan: Vector, scale: number, distance = 50): Point | null {
63 | const lineArray = this.toLineObjects();
64 |
65 | for (let i = 0; i < lineArray.length; i++) {
66 | const pt = lineArray[i].snapNear(p5, pan, scale, distance);
67 | if (pt) {
68 | return pt;
69 | }
70 | }
71 |
72 | return null;
73 | }
74 |
75 | snapMiddle(p5: p5, pan: Vector, scale: number, distance = 30): Point | null {
76 | const lineArray = this.toLineObjects();
77 |
78 | for (let i = 0; i < lineArray.length; i++) {
79 |
80 | const pt = lineArray[i].snapMiddle(p5, pan, scale, distance);
81 | if (pt) {
82 | return pt;
83 | }
84 | }
85 |
86 | return null;
87 | }
88 |
89 | snapEndPoint(p5: p5, pan: Vector, scale: number, distance = 30): Point | null {
90 | const lineArray = this.toLineObjects();
91 |
92 | for (let i = 0; i < lineArray.length; i++) {
93 | const pt = lineArray[i].snapEndPoint(p5, pan, scale, distance);
94 | if (pt) {
95 | return pt;
96 | }
97 | }
98 |
99 | return null;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Canvas/Object/ScaleObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { DrawableObject } from '../../Canvas';
3 | import { ObjectColor } from '../../types';
4 | import { Point } from '../../Geometry';
5 |
6 | export class ScaleObject implements DrawableObject {
7 | isVisible = true
8 | drawCenter: Point;
9 |
10 | constructor(drawCenter = new Point(0, 0)) {
11 | this.drawCenter = drawCenter;
12 | }
13 |
14 | draw(p5: p5, color: ObjectColor, scale: number): void {
15 | const margin = 20 / scale;
16 | const width = p5.windowWidth / scale;
17 | const height = p5.windowHeight / scale;
18 | const start = new Point(
19 | margin - width / 2 + this.drawCenter.x,
20 | height / 2 - 2 * margin + this.drawCenter.y,
21 | );
22 | const length = 5;
23 |
24 | p5.stroke(color.stroke);
25 | p5.strokeWeight(1 / scale);
26 | p5.fill(color.fill);
27 | p5.textSize(15 / scale);
28 |
29 | p5.line(start.x, start.y, start.x + 5, start.y);
30 | p5.strokeWeight(0.5 / scale);
31 | for (let i = 0; i < length + 1; i++) {
32 | p5.line(start.x + i, start.y, start.x + i, start.y - 0.2);
33 |
34 | if (i !== length) {
35 | p5.text(i, start.x + i, start.y - 0.5);
36 | } else {
37 | p5.text(i + 'm', start.x + i, start.y - 0.5);
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Canvas/Object/VectorObject.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { Point, Vector } from '../../Geometry';
3 | import { GeometryObject } from "./GeometryObject";
4 | import { PointObject } from './PointObject';
5 | import { ObjectColor, SnapMode } from '../../types';
6 |
7 | export class VectorObject implements GeometryObject {
8 | /**オブジェクトが保持するジオメトリ情報 */
9 | geometry: Vector;
10 | /**オブジェクトの名前 */
11 | name: string;
12 | /**オブジェクトの所属するレイヤー名 */
13 | layerName: string;
14 | /**オブジェクトのタイプ情報 */
15 | objectType = "Vector";
16 | /**オブジェクト単体に対する可視化のブール値。これとは別に所属するレイヤーに対しても可視化のブール値がある */
17 | isVisible = true;
18 | /**オブジェクトが選択されているかのブール値 */
19 | isSelected = false;
20 |
21 | constructor(geometry: Vector, name = "", layerName = "default") {
22 | this.geometry = geometry;
23 | this.name = name;
24 | this.layerName = layerName;
25 | }
26 |
27 | draw(p5: p5, color: ObjectColor, scale: number, isFill = false, length = 1, weight = 1, origin = Point.zero()): void {
28 | const vec = this.geometry;
29 |
30 | p5.noFill();
31 | p5.stroke(color.stroke);
32 | p5.strokeWeight(weight / scale);
33 |
34 | const from = new Point(origin.x, origin.y);
35 | const to = new Point(vec.x * length + origin.x, vec.y * length + origin.y);
36 | p5.line(from.x, from.y, to.x, to.y);
37 |
38 | const arrow1 = vec.rotate(Math.PI * 5 / 6);
39 | const arrow2 = vec.rotate(Math.PI * 7 / 6);
40 | const aLength = length / 5;
41 |
42 | const aFrom = to;
43 | const aTo1 = new Point(arrow1.x * aLength + aFrom.x, arrow1.y * aLength + aFrom.y);
44 | const aTo2 = new Point(arrow2.x * aLength + aFrom.x, arrow2.y * aLength + aFrom.y);
45 | p5.line(aFrom.x, aFrom.y, aTo1.x, aTo1.y);
46 | p5.line(aFrom.x, aFrom.y, aTo2.x, aTo2.y);
47 | }
48 |
49 | /**
50 | * PointObject としての距離を返す
51 | */
52 | mouseDist(p5: p5, pan: Vector, scale: number): number {
53 | return new PointObject(this.geometry.toPoint()).mouseDist(p5, pan, scale);
54 | }
55 |
56 | /**
57 | * PointObject として snap は処理する
58 | */
59 | snap(p5: p5, snapMode: SnapMode, pan: Vector, scale: number): Point | null {
60 | return new PointObject(this.geometry.toPoint()).snap(p5, snapMode, pan, scale);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Canvas/Object/index.ts:
--------------------------------------------------------------------------------
1 | import { AxisObject } from "./AxisObject";
2 | import { CanvasObject } from "./CanvasObject";
3 | import { GridObject } from "./GridObject";
4 | import { LineObject } from "./LineObject";
5 | import { OrientationObject } from "./OrientationObject";
6 | import { PointObject } from "./PointObject";
7 | import { PolylineObject } from "./PolylineObject";
8 | import { RectangleObject } from "./RectangleObject";
9 | import { ScaleObject } from "./ScaleObject";
10 | import { VectorObject } from "./VectorObject";
11 |
12 | export {
13 | AxisObject,
14 | GridObject,
15 | OrientationObject,
16 | ScaleObject,
17 | CanvasObject,
18 | LineObject,
19 | PointObject,
20 | PolylineObject,
21 | RectangleObject,
22 | VectorObject,
23 | };
24 |
--------------------------------------------------------------------------------
/src/Canvas/index.ts:
--------------------------------------------------------------------------------
1 | import { PointSnap, CurveSnap, DrawableObject } from "./Interface";
2 | import { GeometryObject } from "./Object/GeometryObject";
3 | import { Layer } from "./Layer";
4 | import { CanvasDocument } from "./CanvasDocument";
5 | import { CanvasInfo } from "./CanvasInfo";
6 |
7 | export {
8 | Layer,
9 | CanvasDocument,
10 | CanvasInfo,
11 | };
12 |
13 | export type {
14 | PointSnap,
15 | CurveSnap,
16 | GeometryObject,
17 | DrawableObject,
18 | };
19 |
--------------------------------------------------------------------------------
/src/EditorFunction.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { CanvasDocument } from './Canvas';
3 | import { snap, zoomExtendAll, addRectangle, panCanvas, selectGeometry } from './Function';
4 | import { Vector } from './Geometry';
5 |
6 | interface EditorFunction {
7 | [key: string]: {
8 | caption: string;
9 | isCommand: boolean;
10 | func: (p5: p5, doc: CanvasDocument, args: any) => void;
11 | };
12 | }
13 |
14 | const editorFunction: EditorFunction = {
15 | 'Snap': {
16 | caption: 'Set Snap Information',
17 | isCommand: false,
18 | func: function (p: p5, doc: CanvasDocument): void {
19 | snap(doc, p);
20 | },
21 | },
22 | 'PanCanvas': {
23 | caption: 'Pan Canvas',
24 | isCommand: false,
25 | func: function (p: p5, doc: CanvasDocument, vec: Vector): void {
26 | panCanvas(doc, p, vec);
27 | },
28 | },
29 | 'Select': {
30 | caption: 'Select Geometry',
31 | isCommand: true,
32 | func: function (p: p5, doc: CanvasDocument): void {
33 | selectGeometry(doc, p);
34 | },
35 | },
36 | 'AddRectangle': {
37 | caption: "Add Rectangle",
38 | isCommand: true,
39 | func: function (p: p5, doc: CanvasDocument): void {
40 | addRectangle(doc, p);
41 | },
42 | },
43 | 'ZoomExtendAll': {
44 | caption: "Zoom Extend All",
45 | isCommand: true,
46 | func: function (p: p5, doc: CanvasDocument): void {
47 | zoomExtendAll(doc);
48 | },
49 | },
50 | };
51 |
52 | export { editorFunction };
53 |
--------------------------------------------------------------------------------
/src/Function/AddRectangle.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { Point, Rectangle } from '../Geometry';
3 | import { ObjectColor, Snap } from '../types';
4 | import { PointObject, RectangleObject } from '../Canvas/Object';
5 | import { CanvasDocument, CanvasInfo, GeometryObject, Layer } from '../Canvas';
6 | import { B2DMath } from '../Utils';
7 |
8 |
9 | /**
10 | * Rectangle を描画するとき、マウスの点にサイズを表示する関数
11 | * @param info
12 | * @param p
13 | * @param startPt
14 | * @param endPt
15 | */
16 | function drawRectSizeText(info: CanvasInfo, p: p5, startPt: Point, endPt: Point): void {
17 | let dispNumDecimals;
18 | let dispFactor;
19 |
20 | if (info.unit.length === "m") {
21 | dispFactor = 1000;
22 | dispNumDecimals = info.tolerance - 4;
23 | } else if (info.unit.length === "mm") {
24 | dispFactor = 1;
25 | dispNumDecimals = info.tolerance;
26 | } else {
27 | throw new Error("unknown unit");
28 | }
29 |
30 | const size = 15 / info.scale;
31 | p.fill(0);
32 | p.noStroke();
33 | p.textFont('Helvetica');
34 | const letter =
35 | B2DMath.round(Math.abs(startPt.x - endPt.x) * dispFactor, dispNumDecimals) + "mm X " +
36 | B2DMath.round(Math.abs(startPt.y - endPt.y) * dispFactor, dispNumDecimals) + "mm";
37 | p.textSize(size);
38 | p.text(letter, endPt.x + 5 * size, endPt.y + size);
39 | }
40 |
41 | export function addRectangle(doc: CanvasDocument, p: p5): void {
42 | const info: CanvasInfo = doc.canvasInfo;
43 | const snap: Snap = doc.editorState.snap;
44 | const editingGeometry: GeometryObject[] = doc.editorState.editingGeometry;
45 |
46 | // レイヤーカラーの取得
47 | const activeLayer: Layer | undefined = doc.layers.find(l => l.name === info.activeLayer);
48 | const color: ObjectColor = activeLayer === undefined
49 | ? info.colorSet.find(color => color.name === info.colorMode)!.default
50 | : { stroke: activeLayer.color, fill: activeLayer.color };
51 |
52 | const pt: Point = snap.point === null
53 | ? PointObject.mousePt(p, info.canvasTranslate(), info.scale).geometry
54 | : snap.point;
55 |
56 | if (p.keyIsDown(p.ESCAPE)) {
57 | doc.clearTempGeometry();
58 | } else if (p.mouseIsPressed && p.mouseX > 250 && p.mouseX < p.width - 250) {
59 | if (editingGeometry.length === 0) {
60 | editingGeometry.push(new PointObject(pt));
61 | editingGeometry[0].isVisible = false;
62 | } else {
63 | const startPt: Point = editingGeometry[0].geometry as Point;
64 | new RectangleObject(new Rectangle(startPt, pt))
65 | .draw(p, color, info.scale);
66 | drawRectSizeText(info, p, startPt, pt);
67 | }
68 | } else if (editingGeometry.length > 0) {
69 | const startPt = editingGeometry[0].geometry as Point;
70 | if (pt.distance(startPt) >= info.toleranceValue()) {
71 | doc.geometryObjects.push(
72 | new RectangleObject(
73 | new Rectangle(editingGeometry[0].geometry as Point, pt),
74 | "",
75 | info.activeLayer,
76 | ).toPolylineObject());
77 | }
78 | doc.clearTempGeometry();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Function/PanCanvas.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { Vector } from '../Geometry';
3 | import { CanvasDocument } from '../Canvas';
4 |
5 | export function panCanvas(doc: CanvasDocument, p: p5, vec: Vector | null): void {
6 | const info = doc.canvasInfo;
7 | let pan: Vector;
8 | if (vec !== null) {
9 | pan = vec;
10 | } else {
11 | pan = new Vector(
12 | p.pmouseX - p.mouseX,
13 | p.pmouseY - p.mouseY,
14 | );
15 | }
16 | info.drawCenter = info.drawCenter.add(pan.divide(info.scale).toPoint());
17 |
18 | const canvasObject = doc.canvasObject;
19 | canvasObject.orientation.drawCenter = info.drawCenter;
20 | canvasObject.scaleBar.drawCenter = info.drawCenter;
21 | }
22 |
--------------------------------------------------------------------------------
/src/Function/SelectGeometry.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { PointObject, PolylineObject } from '../Canvas/Object';
3 | import { CanvasDocument } from '../Canvas';
4 |
5 | /**オブジェクトの選択 */
6 | function selectGeometryObjects(doc: CanvasDocument, p: p5) {
7 | // 可視化対象でかつロックされていないレイヤーがあるかの判定
8 | const selectableLayers = doc.layers.filter(layer => layer.isVisible && !layer.isLocked).map(layer => layer.name);
9 | if (selectableLayers.length === 0) {
10 | return;
11 | }
12 | // ポリラインを取得
13 | const plineObj: PolylineObject[] = doc.geometryObjects
14 | .filter(obj => selectableLayers.includes(obj.layerName))
15 | .filter(obj => obj.objectType === 'Polyline') as PolylineObject[];
16 | // ポリラインの内側にマウスの点があった場合は選択に追加
17 | const mousePt = PointObject.mousePt(p, doc.canvasInfo.canvasTranslate(), doc.canvasInfo.scale).geometry;
18 | const selectObj = plineObj
19 | .find(obj => obj.geometry.pointInCurve(mousePt));
20 | if (selectObj !== undefined) {
21 | selectObj.isSelected = true;
22 | }
23 | }
24 |
25 | /** 選択されているオブジェクトを doc の geometryObjects から取り除く*/
26 | function removeGeometryObjects(doc: CanvasDocument) {
27 | doc.geometryObjects = doc.geometryObjects.filter(obj => !obj.isSelected);
28 | }
29 |
30 | /** オブジェクトの選択状態を解除 */
31 | function unselectGeometryObjects(doc: CanvasDocument) {
32 | doc.geometryObjects.forEach(obj => obj.isSelected = false);
33 | }
34 |
35 | /**
36 | * p5js の情報から geometryObject に対して選択されているかのブールを設定する関数
37 | * @param doc
38 | * @param p
39 | * @returns
40 | */
41 | export function selectGeometry(doc: CanvasDocument, p: p5): void {
42 | if (p.mouseIsPressed && p.mouseX > 250 && p.mouseX < p.width - 250 && doc.geometryObjects.length >= 1) {
43 | selectGeometryObjects(doc, p);
44 | } else if (p.keyIsDown(p.BACKSPACE)) {
45 | removeGeometryObjects(doc);
46 | } else if (p.keyIsDown(p.ESCAPE)) {
47 | unselectGeometryObjects(doc);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Function/Snap.test.ts:
--------------------------------------------------------------------------------
1 | import { Point } from '../Geometry/Point';
2 | import { GetSnapAngleVector } from './Snap';
3 |
4 | describe("Snap", () => {
5 | test("GetSnapAngleVector", () => {
6 | const pt0 = new Point(0, 0);
7 |
8 | const pt1 = new Point(1, 1.5);
9 | const angle1 = GetSnapAngleVector(pt1, pt0, 4);
10 | expect(angle1.x).toBeCloseTo(1 / Math.sqrt(2));
11 | expect(angle1.y).toBeCloseTo(1 / Math.sqrt(2));
12 |
13 | const pt2 = new Point(1, -1.5);
14 | const angle2 = GetSnapAngleVector(pt2, pt0, 4);
15 | expect(angle2.x).toBeCloseTo(1 / Math.sqrt(2));
16 | expect(angle2.y).toBeCloseTo(-1 / Math.sqrt(2));
17 | });
18 | });
--------------------------------------------------------------------------------
/src/Function/Snap.ts:
--------------------------------------------------------------------------------
1 | import p5 from 'p5';
2 | import { Line, Point, Vector } from '../Geometry';
3 | import { ObjectColorSet, Snap, SnapMode } from '../types';
4 | import { B2DMath } from '../Utils';
5 | import { LineObject, PointObject } from '../Canvas/Object';
6 | import { CanvasDocument, CanvasInfo, GeometryObject } from '../Canvas';
7 |
8 | function PointSnap(doc: CanvasDocument, p: p5, info: CanvasInfo, minPtDist: number, snap: Snap, color: ObjectColorSet) {
9 | const visibleLayers: string[] = doc.layers
10 | .filter(layer => layer.isVisible)
11 | .map(layer => layer.name);
12 | const visibleObj: GeometryObject[] = doc.geometryObjects
13 | .filter(obj => visibleLayers.includes(obj.layerName));
14 | const distArray: number[] = visibleObj
15 | .map(obj => obj.mouseDist(p, info.canvasTranslate(), info.scale));
16 |
17 | minPtDist = Math.min(...distArray);
18 | const index = distArray.indexOf(minPtDist);
19 | if (index >= 0) {
20 | snap.point = visibleObj[index]
21 | .snap(p, snap.mode, info.canvasTranslate(), info.scale);
22 |
23 | if (snap.point) {
24 | new PointObject(snap.point).draw(p, color.default, info.scale, true, 10);
25 | }
26 | }
27 | snap.objectIndex = doc.geometryObjects.indexOf(visibleObj[index]);
28 | return minPtDist;
29 | }
30 |
31 | function gridSnap(info: CanvasInfo, p: p5, minDist: number, snap: Snap, color: ObjectColorSet) {
32 | const gridInterval = info.gridInterval;
33 | const mousePt: Point = PointObject.mousePt(p, info.canvasTranslate(), info.scale).geometry;
34 | const gridPt: Point = new Point(B2DMath.round(mousePt.x, gridInterval), B2DMath.round(mousePt.y, gridInterval));
35 | const gridDist: number = gridPt.distance(mousePt);
36 |
37 | if ((gridDist < minDist || snap.point === null) &&
38 | p.mouseX > 250 && p.mouseX < p.width - 250) {
39 | snap.point = gridPt;
40 | snap.objectIndex = -1;
41 | new PointObject(snap.point).draw(p, color.default, info.scale, true, 10);
42 | }
43 | }
44 |
45 | /**
46 | * スナップ点とマウスの点から指定した角度のベクトルを返す
47 | * @param mousePt
48 | * @param snapPt
49 | * @param divide pi を何分割するかの設定。45度ごとにスナップする場合は 4
50 | */
51 | export function GetSnapAngleVector(mousePt: Point, snapPt: Point, divide: number): Vector {
52 | const vec: Vector = Vector.from2Points(snapPt, mousePt);
53 | const angleX: number = vec.angle(Vector.unitX());
54 | const angleY: number = vec.angle(Vector.unitY());
55 |
56 | const angles: number[] = [];
57 | const angleSubtract: number[] = [];
58 | for (let i = 0; i < divide + 1; i++) {
59 | angles.push(i * Math.PI / divide);
60 | angleSubtract.push(Math.abs(i * Math.PI / divide - angleX));
61 | }
62 | const min = Math.min(...angleSubtract);
63 | const rotateAngle = angles[angleSubtract.indexOf(min)];
64 |
65 | if (angleY < Math.PI / 2) {
66 | return Vector.unitX().rotate(rotateAngle);
67 | } else {
68 | return Vector.unitX().rotate(-rotateAngle);
69 | }
70 | }
71 |
72 | export function snap(doc: CanvasDocument, p: p5): void {
73 | const info = doc.canvasInfo;
74 | const snap = doc.editorState.snap;
75 | const color: ObjectColorSet = info.colorSet.find(color => color.name === info.colorMode)!;
76 | let minPtDist = Number.MAX_VALUE;
77 |
78 | if (snap.mode.endPoint || snap.mode.middle || snap.mode.near) {
79 | minPtDist = PointSnap(doc, p, info, minPtDist, snap, color);
80 | if (snap.point) {
81 | snap.holdPoint = snap.point;
82 | }
83 | }
84 |
85 | // Angle Snap
86 | if (snap.mode.angle && snap.holdPoint && (snap.mode.endPoint || snap.mode.middle)) {
87 | new PointObject(snap.holdPoint).draw(p, color.default, info.scale, true, 10);
88 | const length = p.windowWidth;
89 | const mousePt = PointObject.mousePt(p, info.canvasTranslate(), info.scale).geometry;
90 |
91 | const angleLine = Line.createFromSDL(snap.holdPoint, GetSnapAngleVector(mousePt, snap.holdPoint, 4), length);
92 | const angleLineObj = new LineObject(angleLine);
93 | angleLineObj.draw(p, color.default, info.scale);
94 | const mode: SnapMode = { endPoint: true, middle: false, near: true, angle: false, grid: false };
95 | const anglePt: Point | null = angleLineObj.snap(p, mode, info.canvasTranslate(), info.scale);
96 | if (anglePt) {
97 | snap.point = anglePt;
98 | new PointObject(anglePt).draw(p, color.default, info.scale, true, 10);
99 | } else {
100 | snap.holdPoint = null;
101 | }
102 | } else {
103 | snap.holdPoint = null;
104 | }
105 |
106 | if (snap.mode.grid) {
107 | gridSnap(info, p, minPtDist, snap, color);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Function/ZoomExtendAll.ts:
--------------------------------------------------------------------------------
1 | import { CanvasScaler, ComputeBoundingBox } from '../Utils';
2 | import { CanvasDocument } from '../Canvas';
3 |
4 | export function zoomExtendAll(doc: CanvasDocument): void {
5 | const info = doc.canvasInfo;
6 | const bbox = ComputeBoundingBox.geometryObjects(doc.geometryObjects, false);
7 | const scaler = new CanvasScaler(info.height, info.width);
8 | info.scale = scaler.scaleFromBoundingBox(bbox);
9 | info.drawCenter = bbox.center();
10 |
11 | const canvasObject = doc.canvasObject;
12 | canvasObject.orientation.drawCenter = info.drawCenter;
13 | canvasObject.scaleBar.drawCenter = info.drawCenter;
14 |
15 | doc.editorState.function = "Select";
16 | }
17 |
--------------------------------------------------------------------------------
/src/Function/index.ts:
--------------------------------------------------------------------------------
1 | import { addRectangle } from "./AddRectangle";
2 | import { panCanvas } from "./PanCanvas";
3 | import { selectGeometry } from "./SelectGeometry";
4 | import { snap } from "./Snap";
5 | import { zoomExtendAll } from "./ZoomExtendAll";
6 |
7 | export {
8 | snap,
9 | zoomExtendAll,
10 | addRectangle,
11 | panCanvas,
12 | selectGeometry,
13 | };
--------------------------------------------------------------------------------
/src/Geometry/GeometryBase.ts:
--------------------------------------------------------------------------------
1 | import { Rectangle } from "./Rectangle";
2 |
3 | /**
4 | * ジオメトリのベースクラス
5 | */
6 | export abstract class GeometryBase {
7 | abstract getBoundingBox(): Rectangle;
8 | }
9 |
--------------------------------------------------------------------------------
/src/Geometry/Intersect.test.ts:
--------------------------------------------------------------------------------
1 | import { Intersect, Line, Point, Vector } from ".";
2 |
3 | describe('line-line', () => {
4 | test('intersect', () => {
5 | const ln1 = new Line(
6 | new Point(1, 0),
7 | new Point(6, 15),
8 | );
9 | const ln2 = new Line(
10 | new Point(5, 5),
11 | new Point(-5, 0),
12 | );
13 |
14 | expect(Intersect.lineLine(ln1, ln2)!.x)
15 | .toBeCloseTo(2.2);
16 | expect(Intersect.lineLine(ln1, ln2)!.y)
17 | .toBeCloseTo(3.6);
18 | });
19 |
20 | test('no intersect', () => {
21 | const ln1 = new Line(
22 | new Point(1, 0),
23 | new Point(8, 5),
24 | );
25 | const ln2 = new Line(
26 | new Point(5, 5),
27 | new Point(-5, 0),
28 | );
29 |
30 | expect(Intersect.lineLine(ln1, ln2))
31 | .toBe(null);
32 | });
33 | });
34 |
35 | describe('vec-line', () => {
36 | test('intersect', () => {
37 | const vec = new Vector(5, 15);
38 | const line = new Line(
39 | new Point(5, 5),
40 | new Point(-5, 5),
41 | );
42 |
43 | expect(Intersect.vectorLine(vec, line, false, false)!.x)
44 | .toBeCloseTo(1.6667);
45 | expect(Intersect.vectorLine(vec, line, false, false)!.y)
46 | .toBeCloseTo(5.00);
47 | });
48 |
49 | test('intersect with short vector', () => {
50 | const vec = new Vector(5, 15).unit();
51 | const line = new Line(
52 | new Point(5, 5),
53 | new Point(-5, 5),
54 | );
55 |
56 | expect(Intersect.vectorLine(vec, line, false, false)!.x)
57 | .toBeCloseTo(1.6667);
58 | expect(Intersect.vectorLine(vec, line, false, false)!.y)
59 | .toBeCloseTo(5.00);
60 | });
61 |
62 | test('no intersect with short vector', () => {
63 | const vec = new Vector(5, 15).unit();
64 | const line = new Line(
65 | new Point(5, 5),
66 | new Point(-5, 5),
67 | );
68 |
69 | expect(Intersect.vectorLine(vec, line, true, false))
70 | .toBe(null);
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/src/Geometry/Intersect.ts:
--------------------------------------------------------------------------------
1 | import { Line, Point, Vector } from ".";
2 |
3 | export class Intersect {
4 | /**
5 | * 2直線の交点を返す
6 | * @param line1
7 | * @param line2
8 | * @returns
9 | */
10 | static lineLine(line1: Line, line2: Line, withinLine1 = true, withinLine2 = true): Point | null {
11 | const vecAB = line1.toVector();
12 | const n1 = vecAB.unit();
13 | const vecCD = line2.toVector();
14 | const n2 = vecCD.unit();
15 | const vecAC = line2.from.subtract(line1.from).toVector();
16 |
17 | const n1AC = n1.dotProduct(vecAC);
18 | const n2AC = n2.dotProduct(vecAC);
19 | const n1n2 = n1.dotProduct(n2);
20 |
21 | // 平行なときは null
22 | if (Math.abs(1.0 - Math.abs(n1n2)) < 1e-5) {
23 | return null;
24 | }
25 |
26 | // d1 は点 A から d2 は点 C からの距離
27 | const d1 = (n1AC - n2AC * n1n2) / (1 - n1n2 * n1n2);
28 | const d2 = (-n2AC + n1AC * n1n2) / (1 - n1n2 * n1n2);
29 |
30 | // 交点が線分中にないときは null
31 | if (d1 < 0 || d2 < 0) {
32 | return null;
33 | } else if (d1 > vecAB.length() && withinLine1) {
34 | return null;
35 | } else if (d2 > vecCD.length() && withinLine2) {
36 | return null;
37 | } else {
38 | return line2.from.add(n2.multiply(d2).toPoint());
39 | }
40 | }
41 |
42 | /**
43 | * Line と Vector の交点を求める
44 | * ( 参考 https://qiita.com/kit2cuz/items/ef93aeb558f353ab479c )
45 | * @param vec
46 | * @param line
47 | * @param withinInput
48 | * @param withinTarget
49 | * @returns
50 | */
51 | static vectorLine(vec: Vector, line: Line, withinInput: boolean, withinTarget: boolean): Point | null {
52 | const vecLine = new Line(Point.zero(), vec.toPoint());
53 | return this.lineLine(vecLine, line, withinInput, withinTarget);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Geometry/Interval.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 数値の幅を表すクラス
3 | */
4 | export class Interval {
5 | private _min: number;
6 | private _max: number;
7 |
8 | constructor(min: number, max: number) {
9 | this._min = min < max ? min : max;
10 | this._max = max > min ? max : min;
11 | }
12 |
13 | get min(): number {
14 | return this._min;
15 | }
16 |
17 | get max(): number {
18 | return this._max;
19 | }
20 |
21 | length(): number {
22 | return this._max - this._min;
23 | }
24 |
25 | mid(): number {
26 | return (this._min + this._max) / 2;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Geometry/Line.test.ts:
--------------------------------------------------------------------------------
1 | import { Vector } from '.';
2 | import { Line } from './Line';
3 | import { Point } from './Point';
4 |
5 | describe('Line', () => {
6 | test('closest point & distance', () => {
7 | const pt1 = new Point(2, 2);
8 | const ln1 = new Line(new Point(0, 0), new Point(5, 0));
9 | expect(ln1.closestPoint(pt1)).toEqual(new Point(2, 0));
10 | expect(ln1.distance(pt1)).toEqual(2);
11 |
12 | const pt2 = new Point(10, 5);
13 | const ln2 = new Line(new Point(0, 0), new Point(5, 0));
14 | expect(ln2.closestPoint(pt2)).toEqual(new Point(5, 0));
15 | expect(ln2.distance(pt2)).toBeCloseTo(7.071);
16 |
17 | const pt3 = new Point(-5, 5);
18 | const ln3 = new Line(new Point(0, 0), new Point(5, 0));
19 | expect(ln3.closestPoint(pt3)).toEqual(new Point(0, 0));
20 | expect(ln3.distance(pt3)).toBeCloseTo(7.071);
21 |
22 | const pt4 = new Point(2, 5);
23 | const ln4 = new Line(new Point(0, 2), new Point(5, 0));
24 | expect(ln4.closestPoint(pt4).x).toBeCloseTo(0.6896);
25 | expect(ln4.closestPoint(pt4).y).toBeCloseTo(1.7241);
26 | expect(ln4.distance(pt4)).toBeCloseTo(3.5282);
27 | });
28 |
29 | test('evaluateClosestPoint', () => {
30 | const point = new Point(2, 6);
31 | const line = new Line(new Point(0, 0), new Point(8, 5));
32 | expect(line.evaluateClosestPoint(point)).toBeCloseTo(0.516);
33 | });
34 |
35 | test('evaluateLine', () => {
36 | const line = new Line(new Point(0, 0), new Point(8, 5));
37 | expect(line.evaluateLine(0.1)!.x).toBeCloseTo(0.8);
38 | expect(line.evaluateLine(0.1)!.y).toBeCloseTo(0.5);
39 | expect(line.evaluateLine(-1)).toBe(null);
40 | });
41 |
42 | test('lineSDL', () => {
43 | const start = new Point(2, 6);
44 | const direction = new Vector(2, 1);
45 | const length = 10;
46 | expect(Line.createFromSDL(start, direction, length).to.x).toBeCloseTo(10.944);
47 | expect(Line.createFromSDL(start, direction, length).to.y).toBeCloseTo(10.472);
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/src/Geometry/Line.ts:
--------------------------------------------------------------------------------
1 | import { LineCoefficient } from '../types';
2 | import { GeometryBase, Point, Rectangle, Vector } from '.';
3 |
4 | /**
5 | * 2D のラインを表すクラス
6 | */
7 | export class Line extends GeometryBase {
8 | from: Point;
9 | to: Point;
10 |
11 | constructor(from: Point, to: Point) {
12 | super();
13 | this.from = from;
14 | this.to = to;
15 | }
16 |
17 | /**
18 | * ax -y + b = 0となる各係数を返す
19 | * @returns
20 | */
21 | calcLineCoefficient(): LineCoefficient {
22 | const to: Point = this.to;
23 | const from: Point = this.from;
24 |
25 | const a = -(to.y - from.y) / (to.x - from.x);
26 | const b = -(a * to.x + to.y);
27 |
28 | return { a: a, b: b };
29 | }
30 |
31 | /**
32 | * ax -y + b = 0 となる係数から法線ベクトル (a,-1) を計算する。
33 | * @returns
34 | */
35 | normal(): Vector {
36 | const lineCoef: LineCoefficient = this.calcLineCoefficient();
37 | return new Vector(lineCoef.a, -1);
38 | }
39 |
40 | /**
41 | * ラインの長さ
42 | * @returns
43 | */
44 | length(): number {
45 | return Math.sqrt(Math.pow(this.to.x - this.from.x, 2) + Math.pow(this.to.y - this.from.y, 2));
46 | }
47 |
48 | /**
49 | * ラインの中点
50 | * @returns
51 | */
52 | middle(): Point {
53 | return this.from.add(this.to).divide(2);
54 | }
55 |
56 | /**
57 | * バウンディングボックスの取得
58 | * @returns
59 | */
60 | getBoundingBox(): Rectangle {
61 | return new Rectangle(this.to, this.from);
62 | }
63 |
64 | /**
65 | * ベクトル化
66 | * @returns
67 | */
68 | toVector(): Vector {
69 | return this.to.subtract(this.from).toVector();
70 | }
71 |
72 | /**
73 | * 与えられた点との Closest Point を返す
74 | * @param point
75 | * @returns
76 | */
77 | closestPoint(point: Point): Point {
78 | const t = this.evaluateClosestPoint(point);
79 |
80 | if (t < 0) {
81 | return this.from;
82 | } else if (t > 1) {
83 | return this.to;
84 | } else {
85 | return this.from.add(this.toVector().multiply(t).toPoint());
86 | }
87 | }
88 |
89 | /**
90 | * 与えられた点との Closest Point の Line 上の位置を返す
91 | * @param point
92 | * @returns
93 | */
94 | evaluateClosestPoint(point: Point): number {
95 | const vecAP = point.subtract(this.from).toVector();
96 | const vecAB = this.toVector();
97 | const APAB = vecAP.dotProduct(vecAB);
98 | const t = APAB / vecAB.length() ** 2;
99 |
100 | return t;
101 | }
102 |
103 | /**
104 | * 与えられた引数を使ってライン上の点を返す
105 | * @param t 0 1 || t < 0) {
110 | return null;
111 | } else {
112 | return this.from.add(this.toVector().multiply(t).toPoint());
113 | }
114 | }
115 |
116 | /**
117 | * 与えられた点とラインの距離を返す
118 | * @param point
119 | * @returns
120 | */
121 | distance(point: Point): number {
122 | return this.closestPoint(point).distance(point);
123 | }
124 |
125 | /**
126 | * 始点、方向、長さからラインを作成する
127 | * @param start
128 | * @param direction
129 | * @param length
130 | * @returns
131 | */
132 | static createFromSDL(start: Point, direction: Vector, length: number): Line {
133 | const end = start.add(direction.unit().multiply(length).toPoint());
134 | return new Line(start, end);
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/Geometry/Point.test.ts:
--------------------------------------------------------------------------------
1 | import { Point } from './Point';
2 |
3 | test('PointTest', () => {
4 | expect(Point.zero()).toEqual(new Point(0, 0));
5 | });
6 |
--------------------------------------------------------------------------------
/src/Geometry/Point.ts:
--------------------------------------------------------------------------------
1 | import { GeometryBase, Rectangle, Vector } from '.';
2 |
3 | /**
4 | * 2D のポイントを表すクラス
5 | */
6 | export class Point extends GeometryBase {
7 | x: number;
8 | y: number;
9 |
10 | constructor(x: number, y: number) {
11 | super();
12 | this.x = x;
13 | this.y = y;
14 | }
15 |
16 | static zero(): Point {
17 | return new Point(0, 0);
18 | }
19 |
20 | static unitX(): Point {
21 | return new Point(1, 0);
22 | }
23 |
24 | static unitY(): Point {
25 | return new Point(0, 1);
26 | }
27 |
28 | distance(pt: Point): number{
29 | return Math.sqrt(Math.pow(this.x - pt.x, 2) + Math.pow(this.y - pt.y, 2));
30 | }
31 |
32 | toVector(): Vector {
33 | return new Vector(this.x, this.y);
34 | }
35 |
36 | add(point: Point): Point {
37 | return new Point(this.x + point.x, this.y + point.y);
38 | }
39 |
40 | subtract(point: Point): Point {
41 | return new Point(this.x - point.x, this.y - point.y);
42 | }
43 |
44 | multiply(number: number): Point {
45 | return new Point(this.x * number, this.y * number);
46 | }
47 |
48 | divide(number: number): Point {
49 | return new Point(this.x / number, this.y / number);
50 | }
51 |
52 | getBoundingBox(): Rectangle {
53 | const e = 0.1;
54 | const pt1 = new Point(this.x - e, this.y - e);
55 | const pt2 = new Point(this.x + e, this.y + e);
56 | return new Rectangle(pt1, pt2);
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Geometry/Polyline.test.ts:
--------------------------------------------------------------------------------
1 | import { Point, Polyline } from ".";
2 |
3 | describe('Polyline', () => {
4 | test('getBoundingBox', () => {
5 | const pLine = new Polyline();
6 | pLine.push(new Point(-8.434585, 3.782531));
7 | pLine.push(new Point(-1.413438, -0.595971));
8 | pLine.push(new Point(4.649102, 2.487116));
9 | pLine.push(new Point(-1.879787, 11.140485));
10 | pLine.push(new Point(-8.408676, 10.674136));
11 |
12 | const bBox = pLine.getBoundingBox();
13 | expect(bBox.maxX()).toBeCloseTo(4.649);
14 | expect(bBox.maxY()).toBeCloseTo(11.14);
15 | expect(bBox.minX()).toBeCloseTo(-8.434);
16 | expect(bBox.minY()).toBeCloseTo(-0.595);
17 | });
18 |
19 | test('pointInCurve', () => {
20 | const pLine = new Polyline();
21 | pLine.push(new Point(-1, -1));
22 | pLine.push(new Point(-1, 1));
23 | pLine.push(new Point(1, 1));
24 | pLine.push(new Point(1, -1));
25 | // 内側
26 | const pt1 = new Point(0, 0);
27 | expect(pLine.pointInCurve(pt1)).toBe(true);
28 | // Line上
29 | const pt2 = new Point(0, -1);
30 | expect(pLine.pointInCurve(pt2)).toBe(true);
31 | // line上(左下角なので2回交差するため false)
32 | const pt3 = new Point(-1, -1);
33 | expect(pLine.pointInCurve(pt3)).toBe(false);
34 | // 外側 交差0
35 | const pt4 = new Point(0, -5);
36 | expect(pLine.pointInCurve(pt4)).toBe(false);
37 | // 外側 交差0
38 | const pt5 = new Point(2, 1);
39 | expect(pLine.pointInCurve(pt5)).toBe(false);
40 | // 外側 交差2
41 | const pt6 = new Point(-2, 0);
42 | expect(pLine.pointInCurve(pt6)).toBe(false);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/Geometry/Polyline.ts:
--------------------------------------------------------------------------------
1 | import { GeometryBase, Point, Line, Rectangle, Intersect } from '.';
2 |
3 | /**
4 | * 2D のポリラインを表すクラス
5 | */
6 | export class Polyline extends GeometryBase {
7 | ptList: Point[] = [];
8 | isClosed = true;
9 |
10 | constructor();
11 | constructor(ptList?: Point[]) {
12 | super();
13 |
14 | if (ptList) {
15 | this.ptList = ptList;
16 | }
17 | }
18 |
19 | count(): number {
20 | return this.ptList.length;
21 | }
22 |
23 | push(pt: Point): void {
24 | this.ptList.push(pt);
25 | }
26 |
27 | pop(): void {
28 | this.ptList.pop();
29 | }
30 |
31 | /**
32 | * ポリラインを線分に変換する。
33 | * @returns
34 | */
35 | toLineArray(): Line[] {
36 | const lineArray: Line[] = [];
37 |
38 | for (let i = 0; i < this.ptList.length - 1; i++) {
39 | lineArray.push(new Line(this.ptList[i], this.ptList[i + 1]));
40 | }
41 | if (this.isClosed) {
42 | lineArray.push(new Line(this.ptList[this.count() - 1], this.ptList[0]));
43 | }
44 |
45 | return lineArray;
46 | }
47 |
48 | /**
49 | * ポリラインを構成する点のリストを取得する。
50 | * @returns
51 | */
52 | toPointArray(): Point[] {
53 | return this.ptList;
54 | }
55 |
56 | /**
57 | * BoundingBox の取得
58 | * @returns
59 | */
60 | getBoundingBox(): Rectangle {
61 | let minX = Number.MAX_VALUE;
62 | let minY = Number.MAX_VALUE;
63 | let maxX = -Number.MAX_VALUE;
64 | let maxY = -Number.MAX_VALUE;
65 |
66 | this.ptList.forEach((pt) => {
67 | maxX = pt.x > maxX ? pt.x : maxX;
68 | maxY = pt.y > maxY ? pt.y : maxY;
69 | minX = pt.x < minX ? pt.x : minX;
70 | minY = pt.y < minY ? pt.y : minY;
71 | });
72 |
73 | return new Rectangle(
74 | new Point(minX, minY),
75 | new Point(maxX, maxY),
76 | );
77 | }
78 |
79 | /**
80 | * Crossing Number Algorithm による点の内外判定を行う。
81 | * 交差数を数えているだけのためカーブ上に点がある場合、正確でない場合がある。
82 | * @param pt 判定対象の点
83 | * @returns 内側にある場合は true を返す。
84 | */
85 | pointInCurve(pt: Point): boolean {
86 | if (this.isClosed === false) {
87 | return false;
88 | }
89 |
90 | const lineArray = this.toLineArray();
91 | const IntersectPtCount = lineArray
92 | .map(line => Intersect.lineLine(line, new Line(pt, pt.add(new Point(1, 0))), true, false))
93 | .filter((pt) => pt !== null)
94 | .length;
95 | return (IntersectPtCount % 2) === 1;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/Geometry/Rectangle.ts:
--------------------------------------------------------------------------------
1 | import { GeometryBase, Interval, Point, Polyline, Line } from '.';
2 |
3 | // TODO: 現在軸に平行な矩形しかかけないので 軸情報をもたせ斜めにも対応する
4 | /**
5 | * 2D の矩形を表すクラス。
6 | */
7 | export class Rectangle extends GeometryBase {
8 | private _ptLL: Point;
9 | private _ptLR: Point;
10 | private _ptUL: Point;
11 | private _ptUR: Point;
12 | private _ptList: Point[] = [];
13 |
14 | constructor(pt1: Point, pt2: Point) {
15 | super();
16 | const minX = Math.min(pt1.x, pt2.x);
17 | const minY = Math.min(pt1.y, pt2.y);
18 | const maxX = Math.max(pt1.x, pt2.x);
19 | const maxY = Math.max(pt1.y, pt2.y);
20 |
21 | this._ptLL = new Point(minX, minY);
22 | this._ptLR = new Point(maxX, minY);
23 | this._ptUL = new Point(minX, maxY);
24 | this._ptUR = new Point(maxX, maxY);
25 | this._ptList = [this._ptLL, this._ptLR, this._ptUR, this._ptUL];
26 | }
27 |
28 | get ptList(): Point[] {
29 | return this._ptList;
30 | }
31 |
32 | hight(): number {
33 | return this.intervalY().length();
34 | }
35 |
36 | width(): number {
37 | return this.intervalX().length();
38 | }
39 |
40 | minX(): number {
41 | return this.intervalX().min;
42 | }
43 |
44 | minY(): number {
45 | return this.intervalY().min;
46 | }
47 |
48 | maxX(): number {
49 | return this.intervalX().max;
50 | }
51 |
52 | maxY(): number {
53 | return this.intervalY().max;
54 | }
55 |
56 | center(): Point {
57 | return new Point(this.intervalX().mid(), this.intervalY().mid());
58 | }
59 |
60 | intervalX(): Interval {
61 | return new Interval(this._ptLL.x, this._ptUR.x);
62 | }
63 |
64 | intervalY(): Interval {
65 | return new Interval(this._ptLL.y, this._ptUR.y);
66 | }
67 |
68 | toPolyline(): Polyline {
69 | const polyline = new Polyline();
70 | polyline.push(this._ptLL);
71 | polyline.push(this._ptLR);
72 | polyline.push(this._ptUR);
73 | polyline.push(this._ptUL);
74 | polyline.isClosed = true;
75 | return polyline;
76 | }
77 |
78 | toLineArray(): Line[] {
79 | return this.toPolyline().toLineArray();
80 | }
81 |
82 | area(): number {
83 | return this.hight() * this.width();
84 | }
85 |
86 | getBoundingBox(): Rectangle {
87 | return this;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Geometry/Vector.test.ts:
--------------------------------------------------------------------------------
1 | import { Point, Vector } from ".";
2 |
3 | describe('Vector', () => {
4 | const vec = new Vector(1, 1);
5 |
6 | test('from2pt', () => {
7 | const vec2 = Vector.from2Points(new Point(0, 0), new Point(1, 1));
8 | expect(vec2.x).toBe(1);
9 | expect(vec2.y).toBe(1);
10 | const vec3 = Vector.from2Points(new Point(1, 1), new Point(0, 0));
11 | expect(vec3.x).toBe(-1);
12 | expect(vec3.y).toBe(-1);
13 | });
14 |
15 | test('rotate', () => {
16 | expect(vec.rotate(2).x).toBeCloseTo(-1.325);
17 | expect(vec.rotate(2).y).toBeCloseTo(0.493);
18 | expect(vec.rotate(Math.PI / 2).x).toBeCloseTo(-1);
19 | expect(vec.rotate(Math.PI / 2).y).toBeCloseTo(1);
20 | expect(vec.rotate(-Math.PI / 2).x).toBeCloseTo(1);
21 | expect(vec.rotate(-Math.PI / 2).y).toBeCloseTo(-1);
22 | });
23 |
24 | test('add', () => {
25 | expect(vec.add(vec)).toEqual(new Vector(2, 2));
26 | });
27 |
28 | test('subtract', () => {
29 | expect(vec.subtract(vec)).toEqual(Vector.zero());
30 | });
31 |
32 | test('multiply', () => {
33 | expect(vec.multiply(4)).toEqual(new Vector(4, 4));
34 | });
35 |
36 | test('divide', () => {
37 | expect(vec.divide(4)).toEqual(new Vector(0.25, 0.25));
38 | });
39 |
40 | test('dotProduct', () => {
41 | const vec2 = new Vector(2, 5.5);
42 | expect(vec.dotProduct(vec2)).toEqual(7.5);
43 | });
44 |
45 | test('angle', () => {
46 | const vec2 = new Vector(-1, 1);
47 | const vec3 = new Vector(-1, -1);
48 | const vec4 = new Vector(1, -1);
49 | const unitX = Vector.unitX();
50 | const unitY = Vector.unitY();
51 | expect(vec.angle(unitX)).toBeCloseTo(Math.PI / 4);
52 | expect(vec.angle(unitY)).toBeCloseTo(Math.PI / 4);
53 | expect(vec2.angle(unitX)).toBeCloseTo(3 * Math.PI / 4);
54 | expect(vec2.angle(unitY)).toBeCloseTo(Math.PI / 4);
55 | expect(vec3.angle(unitX)).toBeCloseTo(3 * Math.PI / 4);
56 | expect(vec3.angle(unitY)).toBeCloseTo(3 * Math.PI / 4);
57 | expect(vec4.angle(unitX)).toBeCloseTo(Math.PI / 4);
58 | expect(vec4.angle(unitY)).toBeCloseTo(3 * Math.PI / 4);
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/src/Geometry/Vector.ts:
--------------------------------------------------------------------------------
1 | import { GeometryBase, Point, Rectangle } from '.';
2 |
3 | /**
4 | * 2D のベクトルを表すクラス
5 | */
6 | export class Vector extends GeometryBase {
7 | x: number;
8 | y: number;
9 |
10 | constructor(x: number, y: number) {
11 | super();
12 | this.x = x;
13 | this.y = y;
14 | }
15 |
16 | /**
17 | * p1 から p2 に向かうベクトルを返す
18 | * @param p1
19 | * @param p2
20 | * @returns
21 | */
22 | static from2Points(p1: Point, p2: Point): Vector {
23 | return new Vector(p2.x - p1.x, p2.y - p1.y);
24 | }
25 |
26 | /**
27 | * ゼロベクトルを返す
28 | * @returns
29 | */
30 | static zero(): Vector {
31 | return new Vector(0, 0);
32 | }
33 |
34 | /**
35 | * X 方向の単位ベクトルを返す
36 | * @returns
37 | */
38 | static unitX(): Vector {
39 | return new Vector(1, 0);
40 | }
41 |
42 | /**
43 | * Y 方向の単位ベクトルを返す
44 | * @returns
45 | */
46 | static unitY(): Vector {
47 | return new Vector(0, 1);
48 | }
49 |
50 | /**
51 | * ベクトルを単位ベクトル化する
52 | * @returns
53 | */
54 | unit(): Vector {
55 | return new Vector(this.x / this.length(), this.y / this.length());
56 | }
57 |
58 | /**
59 | * ベクトルの長さを返す
60 | * @returns
61 | */
62 | length(): number {
63 | return Math.sqrt(this.x ** 2 + this.y ** 2);
64 | }
65 |
66 | /**
67 | * 入力の角度(radian)分だけ、ベクトルを回転させる
68 | * 正の値が時計回り
69 | * @param radian
70 | * @returns
71 | */
72 | rotate(radian: number): Vector {
73 | const cos = Math.cos(radian);
74 | const sin = Math.sin(radian);
75 | const x = this.x * cos - this.y * sin;
76 | const y = this.x * sin + this.y * cos;
77 | return new Vector(x, y);
78 | }
79 |
80 | /**
81 | * 成分ごとの和を返す
82 | * @param vector
83 | * @returns
84 | */
85 | add(vector: Vector): Vector {
86 | return new Vector(this.x + vector.x, this.y + vector.y);
87 | }
88 |
89 | /**
90 | * 成分ごとの差を返す
91 | * @param vector
92 | * @returns
93 | */
94 | subtract(vector: Vector): Vector {
95 | return new Vector(this.x - vector.x, this.y - vector.y);
96 | }
97 |
98 | /**
99 | * 各成分を入力のスカラー倍にする
100 | * @param number
101 | * @returns
102 | */
103 | multiply(number: number): Vector {
104 | return new Vector(this.x * number, this.y * number);
105 | }
106 |
107 | /**
108 | * 各成分を入力のスカラーで割る
109 | * @param number
110 | * @returns
111 | */
112 | divide(number: number): Vector {
113 | return new Vector(this.x / number, this.y / number);
114 | }
115 |
116 | /**
117 | * 内積を返す
118 | * @param vector
119 | * @returns
120 | */
121 | dotProduct(vector: Vector): number {
122 | return this.x * vector.x + this.y * vector.y;
123 | }
124 |
125 | /**
126 | * 引数のベクトルとの角度を返す
127 | * @param vector
128 | * @returns 0 ~ π の値
129 | */
130 | angle(vector: Vector): number {
131 | const cos = this.dotProduct(vector) / (this.length() * vector.length());
132 | return Math.acos(cos);
133 | }
134 |
135 | /**
136 | * ベクトル成分を持つ Point を返す
137 | * @returns
138 | */
139 | toPoint(): Point {
140 | return new Point(this.x, this.y);
141 | }
142 |
143 | getBoundingBox(): Rectangle {
144 | return this.toPoint().getBoundingBox();
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/Geometry/index.ts:
--------------------------------------------------------------------------------
1 | import { GeometryBase } from "./GeometryBase";
2 | import { Polyline } from "./Polyline";
3 | import { Line } from "./Line";
4 | import { Vector } from "./Vector";
5 | import { Point } from "./Point";
6 | import { Interval } from "./Interval";
7 | import { Rectangle } from "./Rectangle";
8 | import { Intersect } from "./Intersect";
9 |
10 | export {
11 | GeometryBase,
12 | Interval,
13 | Point,
14 | Line,
15 | Polyline,
16 | Rectangle,
17 | Vector,
18 | Intersect,
19 | };
--------------------------------------------------------------------------------
/src/P5Canvas.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import _p5 from 'p5';
3 | import { useP5Context } from './P5Context';
4 |
5 | interface Props {
6 | sketch: (p: _p5) => void;
7 | }
8 |
9 | export default function P5Canvas({ sketch }: Props): React.ReactElement {
10 | const { p5, setP5, setCanvasWidth, setCanvasHeight, ref } = useP5Context();
11 | useEffect(() => {
12 | setP5(new _p5(sketch));
13 |
14 | return () => {
15 | if (p5) {
16 | p5.remove();
17 | }
18 | };
19 | }, [sketch]);
20 |
21 | useEffect(() => {
22 | function onResize() {
23 | setCanvasWidth(window.innerWidth);
24 | setCanvasHeight(window.innerHeight);
25 | }
26 | onResize();
27 |
28 | window.addEventListener('resize', onResize);
29 |
30 | return () => window.removeEventListener('resize', onResize);
31 | }, [p5, ref]);
32 |
33 | return ;
34 | }
35 |
--------------------------------------------------------------------------------
/src/P5Context.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useMemo, useState, useRef, RefObject } from 'react';
2 | import _p5 from 'p5';
3 |
4 | interface P5State {
5 | p5: _p5 | null;
6 | setP5: (p5: _p5) => void;
7 | canvasWidth: number;
8 | setCanvasWidth: (canvasWidth: number) => void;
9 | canvasHeight: number;
10 | setCanvasHeight: (canvasHeight: number) => void;
11 | ref: RefObject | null;
12 | }
13 |
14 | const initialState = {
15 | p5: null,
16 | setP5: () => { },
17 | canvasWidth: 2000,
18 | setCanvasWidth: () => { },
19 | canvasHeight: 1000,
20 | setCanvasHeight: () => { },
21 | ref: null,
22 | };
23 |
24 | const P5Context = React.createContext(initialState);
25 |
26 | interface P5ProviderProps {
27 | children: React.ReactNode;
28 | }
29 |
30 | export function P5Provider({ children }: P5ProviderProps): React.ReactElement {
31 | const ref = useRef(null);
32 | const [p5, setP5] = useState<_p5 | null>(initialState.p5);
33 | const [canvasWidth, setCanvasWidth] = useState(initialState.canvasWidth);
34 | const [canvasHeight, setCanvasHeight] = useState(initialState.canvasHeight);
35 |
36 | const state: P5State = useMemo(() => {
37 | return {
38 | p5,
39 | setP5,
40 | canvasWidth,
41 | setCanvasWidth,
42 | canvasHeight,
43 | setCanvasHeight,
44 | ref,
45 | };
46 | }, [canvasHeight, canvasWidth, p5]);
47 |
48 | return {children};
49 | }
50 |
51 | export function useP5Context(): P5State {
52 | return useContext(P5Context);
53 | }
54 |
--------------------------------------------------------------------------------
/src/Utils/ComputeBoundingBox.ts:
--------------------------------------------------------------------------------
1 | import { GeometryObject } from "../Canvas";
2 | import { Point, Rectangle } from "../Geometry";
3 |
4 | export class ComputeBoundingBox {
5 | static geometryObjects(geometryObjects: GeometryObject[], includeOrigin: boolean): Rectangle {
6 | let minX = Number.MAX_VALUE;
7 | let minY = Number.MAX_VALUE;
8 | let maxX = -Number.MAX_VALUE;
9 | let maxY = -Number.MAX_VALUE;
10 |
11 | geometryObjects.forEach((object) => {
12 | const boundingBox = object.geometry.getBoundingBox();
13 | const intervalX = boundingBox.intervalX();
14 | const intervalY = boundingBox.intervalY();
15 | maxX = intervalX.max > maxX ? intervalX.max : maxX;
16 | maxY = intervalY.max > maxY ? intervalY.max : maxY;
17 | minX = intervalX.min < minX ? intervalX.min : minX;
18 | minY = intervalY.min < minY ? intervalY.min : minY;
19 | });
20 | if (includeOrigin) {
21 | maxX = 0 > maxX ? 0 : maxX;
22 | maxY = 0 > maxY ? 0 : maxY;
23 | minX = 0 < minX ? 0 : minX;
24 | minY = 0 < minY ? 0 : minY;
25 | }
26 |
27 | return new Rectangle(new Point(minX, minY), new Point(maxX, maxY));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Utils/Math.test.ts:
--------------------------------------------------------------------------------
1 | import { round } from './Math';
2 |
3 | describe("Math", () => {
4 | test('round', () => {
5 | expect(round(123.4567, 4)).toBe(123.4567);
6 | expect(round(123.4567, 2)).toBe(123.46);
7 | expect(round(123.4567, 0)).toBe(123);
8 | expect(round(123.4567, -2)).toBe(100);
9 | });
10 | });
--------------------------------------------------------------------------------
/src/Utils/Math.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 入力の桁数になるようで四捨五入する関数
3 | * @param num
4 | * @param decimals
5 | * @returns (1.234, 2) を入力した場合 1.23
6 | */
7 | function round(num: number, decimals: number): number {
8 | return Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals);
9 | }
10 |
11 | export { round };
12 |
--------------------------------------------------------------------------------
/src/Utils/Scale.ts:
--------------------------------------------------------------------------------
1 | import { Point, Rectangle } from "../Geometry";
2 |
3 | export class CanvasScaler {
4 | canvasHeight: number;
5 | canvasWidth: number;
6 | scale: number;
7 |
8 | constructor(canvasHeight: number, canvasWidth: number, scale = 1) {
9 | this.canvasHeight = canvasHeight;
10 | this.canvasWidth = canvasWidth;
11 | this.scale = scale;
12 | }
13 |
14 | setScale(boundingBox: Rectangle): number {
15 | const bBoxWidth = boundingBox.width();
16 | const bBoxHeight = boundingBox.hight();
17 |
18 | const maxRatio = 0.5;
19 | const maxWidth = this.canvasWidth * maxRatio;
20 | const maxHeight = this.canvasHeight * maxRatio;
21 | this.scale = Math.min(maxWidth / bBoxWidth, maxHeight / bBoxHeight);
22 |
23 | return this.scale;
24 | }
25 |
26 | scaledCenterPoint(boundingBox: Rectangle): Point {
27 | const minX = boundingBox.minX();
28 | const minY = boundingBox.minY();
29 |
30 | const bBoxWidth = boundingBox.width();
31 | const bBoxHeight = boundingBox.hight();
32 | const baseShiftX = -minX * this.scale;
33 | const baseShiftY = -minY * this.scale;
34 |
35 | const shiftX = this.canvasWidth / 2 - bBoxWidth * this.scale / 2 + baseShiftX;
36 | const shiftY = this.canvasHeight / 2 - bBoxHeight * this.scale / 2 + baseShiftY;
37 |
38 | return new Point(shiftX, shiftY);
39 | }
40 |
41 | scaleFromBoundingBox(boundingBox: Rectangle): number {
42 | return this.setScale(boundingBox);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Utils/index.ts:
--------------------------------------------------------------------------------
1 | import { ComputeBoundingBox } from './ComputeBoundingBox';
2 | import { CanvasScaler } from './Scale';
3 | import * as B2DMath from './Math';
4 |
5 | export {
6 | ComputeBoundingBox,
7 | CanvasScaler,
8 | B2DMath,
9 | };
10 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import P5Canvas from './P5Canvas';
2 | import { P5Provider, useP5Context } from './P5Context';
3 |
4 | export {
5 | P5Canvas,
6 | P5Provider,
7 | useP5Context,
8 | };
9 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { Point } from "./Geometry";
2 |
3 | /**
4 | * y = ax + b のとなる各係数
5 | */
6 | export type LineCoefficient = {
7 | a: number;
8 | b: number;
9 | };
10 |
11 | /**
12 | * カラーモードごとの色情報
13 | */
14 | export type ObjectColorSet = {
15 | name: string;
16 | default: ObjectColor
17 | select: ObjectColor
18 | axis: ObjectColor
19 | grid: ObjectColor
20 | scaleBar: ObjectColor
21 | orientation: ObjectColor
22 | };
23 |
24 | /**
25 | * objectのstrokeとfillの色情報
26 | */
27 | export type ObjectColor = {
28 | stroke: string;
29 | fill: string;
30 | }
31 |
32 | /**
33 | * スナップのモード、スナップ点、スナップされたオブジェクトのインデックスを保持するタイプ
34 | */
35 | export type Snap = {
36 | mode: SnapMode
37 | point: Point | null;
38 | holdPoint: Point | null;
39 | objectIndex: number | null;
40 | };
41 |
42 | /**
43 | * 各スナップモードのブール値
44 | */
45 | export type SnapMode = {
46 | endPoint: boolean,
47 | near: boolean,
48 | middle: boolean,
49 | angle: boolean,
50 | grid: boolean,
51 | }
52 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | .footer {
16 | display: flex;
17 | flex: 1;
18 | padding: 2rem 0;
19 | border-top: 1px solid #eaeaea;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 |
24 | .footer a {
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | flex-grow: 1;
29 | }
30 |
31 | .title a {
32 | color: #0070f3;
33 | text-decoration: none;
34 | }
35 |
36 | .title a:hover,
37 | .title a:focus,
38 | .title a:active {
39 | text-decoration: underline;
40 | }
41 |
42 | .title {
43 | margin: 0;
44 | line-height: 1.15;
45 | font-size: 4rem;
46 | }
47 |
48 | .title,
49 | .description {
50 | text-align: center;
51 | }
52 |
53 | .description {
54 | margin: 4rem 0;
55 | line-height: 1.5;
56 | font-size: 1.5rem;
57 | }
58 |
59 | .code {
60 | background: #fafafa;
61 | border-radius: 5px;
62 | padding: 0.75rem;
63 | font-size: 1.1rem;
64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
65 | Bitstream Vera Sans Mono, Courier New, monospace;
66 | }
67 |
68 | .grid {
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 | flex-wrap: wrap;
73 | max-width: 800px;
74 | }
75 |
76 | .card {
77 | margin: 1rem;
78 | padding: 1.5rem;
79 | text-align: left;
80 | color: inherit;
81 | text-decoration: none;
82 | border: 1px solid #eaeaea;
83 | border-radius: 10px;
84 | transition: color 0.15s ease, border-color 0.15s ease;
85 | max-width: 300px;
86 | }
87 |
88 | .card:hover,
89 | .card:focus,
90 | .card:active {
91 | color: #0070f3;
92 | border-color: #0070f3;
93 | }
94 |
95 | .card h2 {
96 | margin: 0 0 1rem 0;
97 | font-size: 1.5rem;
98 | }
99 |
100 | .card p {
101 | margin: 0;
102 | font-size: 1.25rem;
103 | line-height: 1.5;
104 | }
105 |
106 | .logo {
107 | height: 1em;
108 | margin-left: 0.5rem;
109 | }
110 |
111 | @media (max-width: 600px) {
112 | .grid {
113 | width: 100%;
114 | flex-direction: column;
115 | }
116 | }
117 |
118 | @media (prefers-color-scheme: dark) {
119 | .card,
120 | .footer {
121 | border-color: #222;
122 | }
123 | .code {
124 | background: #111;
125 | }
126 | .logo img {
127 | filter: invert(1);
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
18 | @media (prefers-color-scheme: dark) {
19 | html {
20 | color-scheme: dark;
21 | }
22 | body {
23 | color: white;
24 | background: black;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true
21 | },
22 | "include": [
23 | "next-env.d.ts",
24 | "**/*.ts",
25 | "**/*.tsx"
26 | ],
27 | "exclude": [
28 | "node_modules"
29 | ]
30 | }
--------------------------------------------------------------------------------