├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── JestRunner.yml │ └── gh-pages.yml ├── .gitignore ├── LICENSE ├── README.md ├── editor-ui ├── EditPlansCanvas.tsx ├── EditPlansContext.tsx ├── InputSidebar.tsx ├── LayerContainer.tsx ├── LayerItem.tsx ├── NavSidebar.tsx ├── Sidebar │ ├── Sidebar.tsx │ ├── SidebarContext.tsx │ ├── index.tsx │ └── useWindowSize.ts └── index.tsx ├── jest.config.js ├── next.config.js ├── package.json ├── pages └── index.tsx ├── public ├── .nojekyll ├── favicon.ico └── vercel.svg ├── src ├── Canvas │ ├── CanvasDocument.ts │ ├── CanvasInfo.ts │ ├── EditorState.ts │ ├── Interface.ts │ ├── Layer.ts │ ├── Object │ │ ├── AxisObject.ts │ │ ├── CanvasObject.ts │ │ ├── GeometryObject.ts │ │ ├── GridObject.ts │ │ ├── LineObject.ts │ │ ├── OrientationObject.ts │ │ ├── PointObject.ts │ │ ├── PolylineObject.ts │ │ ├── RectangleObject.ts │ │ ├── ScaleObject.ts │ │ ├── VectorObject.ts │ │ └── index.ts │ └── index.ts ├── EditorFunction.ts ├── Function │ ├── AddRectangle.ts │ ├── PanCanvas.ts │ ├── SelectGeometry.ts │ ├── Snap.test.ts │ ├── Snap.ts │ ├── ZoomExtendAll.ts │ └── index.ts ├── Geometry │ ├── GeometryBase.ts │ ├── Intersect.test.ts │ ├── Intersect.ts │ ├── Interval.ts │ ├── Line.test.ts │ ├── Line.ts │ ├── Point.test.ts │ ├── Point.ts │ ├── Polyline.test.ts │ ├── Polyline.ts │ ├── Rectangle.ts │ ├── Vector.test.ts │ ├── Vector.ts │ └── index.ts ├── P5Canvas.tsx ├── P5Context.tsx ├── Utils │ ├── ComputeBoundingBox.ts │ ├── Math.test.ts │ ├── Math.ts │ ├── Scale.ts │ └── index.ts ├── index.ts └── types.ts ├── styles ├── Home.module.css └── globals.css ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": ["plugin:react-hooks/recommended", "next/core-web-vitals"], 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["react-hooks"], 19 | "rules": { 20 | "require-jsdoc": 0, 21 | "max-len": ["error", { "code": 200 }], 22 | "semi": [2, "always"], 23 | "space-before-function-paren": 0, 24 | "comma-dangle": ["error", "always-multiline"], 25 | "no-case-declarations": "off", 26 | "quote-props": 0, 27 | "react/display-name": ["off", { "ignoreTranspilerName": false }], 28 | "react-hooks/rules-of-hooks": "error", 29 | "react-hooks/exhaustive-deps": "warn" 30 | }, 31 | "overrides": [ 32 | { 33 | "files": ["*.ts", "*.tsx"], 34 | "extends": ["plugin:@typescript-eslint/recommended"], 35 | "parser": "@typescript-eslint/parser", 36 | "plugins": ["@typescript-eslint"], 37 | "rules": { 38 | "no-empty-function": "off", 39 | "@typescript-eslint/no-empty-function": "warn", 40 | "@typescript-eslint/ban-ts-comment": "warn", 41 | "@typescript-eslint/no-this-alias": [ 42 | "error", 43 | { 44 | "allowDestructuring": true, 45 | "allowedNames": ["scope"] 46 | } 47 | ], 48 | "@typescript-eslint/no-non-null-assertion": "off", 49 | "no-use-before-define": "off", 50 | "@typescript-eslint/no-use-before-define": ["error"], 51 | "react/prop-types": "off" 52 | } 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Describe the bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 1. Click on '....' 19 | 1. Scroll down to '....' 20 | 1. See error 21 | 22 | ## Expected behavior 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | ## Screenshots 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | ## Environment 31 | 32 | - OS: [e.g. Windows] 33 | - Rhino Version [e.g. v7.14] 34 | 35 | ## Additional context 36 | 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Is your feature request related to a problem? Please describe. 10 | 11 | A clear and concise description of what the problem is. 12 | ex. I'm always frustrated when [...] 13 | 14 | ## Describe the solution you'd like 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | ## Describe alternatives you've considered 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | ## Additional context 23 | 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Added 2 | 3 | ## Changed 4 | 5 | ## Deprecated 6 | 7 | ## Removed 8 | 9 | ## Fixed 10 | 11 | ## Security 12 | 13 | ## Related issue number 14 | 15 | close # 16 | -------------------------------------------------------------------------------- /.github/workflows/JestRunner.yml: -------------------------------------------------------------------------------- 1 | name: JestRunner 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [16.x] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | cache: yarn 18 | - name: Install dependencies 19 | run: yarn --frozen-lockfile 20 | - name: Run test 21 | run: yarn test -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | # main ブランチ の push 時にこのワークフローを実行する 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # main ブランチを取得する 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | # Node.js のセットアップをする 19 | - name: Setup Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 16 23 | cache: yarn 24 | 25 | # パッケージをインストールする 26 | - name: Install dependencies 27 | run: yarn --frozen-lockfile 28 | 29 | # ビルドする 30 | - name: Build 31 | run: yarn cibuild 32 | 33 | # GitHub Pages にデプロイする 34 | - name: Deploy 35 | uses: peaceiris/actions-gh-pages@v3 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: out -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 BAUES Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building Editor 2d 2 | 3 | Simple 2D CAD wrapped in p5js. 4 | It implements command input, layers, and classes for handling shapes, just like CAD software. 5 | 6 | The main processing as CAD is in the `/src` directory. 7 | Other directories are Next.js related files. 8 | 9 | ## Getting Started 10 | 11 | First, run the development server: 12 | 13 | ```bash 14 | yarn dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | ## CAD Usage 20 | 21 | https://user-images.githubusercontent.com/23289252/209470343-e5917129-d843-439b-a71f-f81eed050bd4.mov 22 | 23 | ## Feature 24 | 25 | - CAD UI Feature 26 | - Select Geometry 27 | - Delete Geometry 28 | - Add rectangle 29 | - Zoom Extend All 30 | - Snap 31 | - End point 32 | - Middle 33 | - Near 34 | - Grid 35 | - Angle 36 | - scale bar 37 | - Layer 38 | - Visible 39 | - Lock 40 | - Layer color select 41 | - Active 42 | - Add & Delete 43 | - Pan 44 | - Zoom 45 | - Geometry 46 | - Line 47 | - Point 48 | - Polyline 49 | - Rectangle 50 | - Vector 51 | -------------------------------------------------------------------------------- /editor-ui/EditPlansCanvas.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import p5 from 'p5'; 3 | import Box from '@mui/material/Box'; 4 | import { useTheme } from '@mui/material/styles'; 5 | import { useEditPlansContext } from './EditPlansContext'; 6 | import { P5Canvas, useP5Context } from '../src'; 7 | import { ComputeBoundingBox, CanvasScaler } from '../src/Utils'; 8 | import { CanvasInfo, CanvasDocument, GeometryObject, Layer } from '../src/Canvas'; 9 | import { AxisObject, CanvasObject, GridObject, OrientationObject, PolylineObject, ScaleObject } from '../src/Canvas/Object'; 10 | import { editorFunction } from '../src/EditorFunction'; 11 | import { Point, Rectangle, Vector } from '../src/Geometry'; 12 | 13 | let doc: CanvasDocument = new CanvasDocument(new CanvasInfo(0, 0, 0), new CanvasObject(), [], []); 14 | 15 | /** 16 | * Orientation と ScaleBar の表示の設定 17 | * @param isVisible 18 | */ 19 | function OrientationAndScaleBarVisibility(canvasDoc: CanvasDocument, isVisible: boolean) { 20 | canvasDoc.canvasObject.orientation.isVisible = isVisible; 21 | canvasDoc.canvasObject.scaleBar.isVisible = isVisible; 22 | } 23 | 24 | function panCanvasByMouse(p: p5) { 25 | if (p.mouseIsPressed) { 26 | OrientationAndScaleBarVisibility(doc, false); 27 | editorFunction['PanCanvas'].func(p, doc, null); 28 | } else { 29 | OrientationAndScaleBarVisibility(doc, true); 30 | } 31 | } 32 | 33 | function panCanvasByArrowKey(p: p5, doc: CanvasDocument) { 34 | const value = 10; 35 | 36 | if (!doc.canvasInfo.isActive) { 37 | return; 38 | } 39 | 40 | if (p.keyIsDown(p.DOWN_ARROW)) { // 矢印キーを押したときキャンバスを動かす 41 | editorFunction['PanCanvas'].func(p, doc, new Vector(0, -value)); 42 | } else if (p.keyIsDown(p.UP_ARROW)) { 43 | editorFunction['PanCanvas'].func(p, doc, new Vector(0, value)); 44 | } else if (p.keyIsDown(p.LEFT_ARROW)) { 45 | editorFunction['PanCanvas'].func(p, doc, new Vector(value, 0)); 46 | } else if (p.keyIsDown(p.RIGHT_ARROW)) { 47 | editorFunction['PanCanvas'].func(p, doc, new Vector(-value, 0)); 48 | } 49 | } 50 | 51 | /** 52 | * p5js の setup と draw の定義 53 | */ 54 | const sketch = (p: p5) => { 55 | p.setup = () => { 56 | p.createCanvas(window.innerWidth, window.innerHeight); 57 | p.rectMode(p.CENTER); 58 | p.textAlign(p.CENTER, p.CENTER); 59 | }; 60 | 61 | p.draw = () => { 62 | const info = doc.canvasInfo; 63 | 64 | p.resizeCanvas(info.width, info.height); 65 | // @ts-ignore 66 | p.clear(); 67 | p.translate(info.canvasTranslate().x, info.canvasTranslate().y); 68 | p.scale(info.scale); 69 | 70 | if (p.keyIsDown(p.SHIFT)) { 71 | panCanvasByMouse(p); 72 | } else { 73 | editorFunction['Snap'].func(p, doc, []); 74 | const funcKey = doc.editorState.function; 75 | editorFunction[funcKey].func(p, doc, []); 76 | } 77 | 78 | doc.draw(p); 79 | }; 80 | 81 | // キャンバスの拡大縮小 82 | p.mouseWheel = (event: WheelEvent) => { 83 | doc.canvasInfo.scale = Math.sign(event.deltaY) > 0 84 | ? doc.canvasInfo.scale * 0.9 85 | : doc.canvasInfo.scale * 1.1; 86 | p.scale(doc.canvasInfo.scale); 87 | }; 88 | 89 | p.keyPressed = () => { 90 | if (p.keyIsDown(p.SHIFT)) { // 手を表示してパンモードであることを示す 91 | p.cursor(p.HAND); 92 | } else if (p.keyIsDown(p.DOWN_ARROW) || p.keyIsDown(p.UP_ARROW) || p.keyIsDown(p.LEFT_ARROW) || p.keyIsDown(p.RIGHT_ARROW)) { 93 | panCanvasByArrowKey(p, doc); // 矢印キーを押したときキャンバスを動かす 94 | } 95 | }; 96 | 97 | p.keyReleased = () => { 98 | p.cursor(p.ARROW); 99 | return false; 100 | }; 101 | }; 102 | 103 | export default function EditPlansCanvas(): React.ReactElement { 104 | const theme = useTheme(); 105 | const { canvasWidth, canvasHeight } = useP5Context(); 106 | const { floor, setFloor, northAxis, northAxisError, editFunction, snapMode, layers, setLayers, setDocument, active, setActive } = useEditPlansContext(); 107 | 108 | // キャンバスのイニシャライズ 109 | useEffect(() => { 110 | const info = new CanvasInfo(1, canvasHeight, canvasWidth); 111 | const bbox = new Rectangle(new Point(-1.5, -1.5), new Point(1.5, 1.5)); 112 | const scaler = new CanvasScaler(info.height, info.width); 113 | info.drawCenter = bbox.center(); 114 | info.colorMode = theme.palette.mode; 115 | info.scale = scaler.scaleFromBoundingBox(bbox); 116 | 117 | const geometries: GeometryObject[] = [new PolylineObject(bbox.toPolyline())]; 118 | const angle = (northAxisError) ? 0 : northAxis; 119 | const orientation = new OrientationObject(angle, info.drawCenter); 120 | const scaleObject = new ScaleObject(info.drawCenter); 121 | const canvasObject = new CanvasObject(new GridObject(20, 5, 5), new AxisObject(), scaleObject, orientation); 122 | 123 | const layers: Layer[] = [new Layer("default", true, 1, 3, false)]; 124 | setLayers(layers); 125 | info.activeLayer = "default"; 126 | setFloor("default"); 127 | 128 | doc = new CanvasDocument(info, canvasObject, geometries, layers); 129 | setDocument(doc); 130 | }, []); 131 | 132 | // 編集モードの変更 133 | useEffect(() => { 134 | doc.editorState.function = editFunction; 135 | }, [editFunction]); 136 | 137 | // スナップモードの変更 138 | useEffect(() => { 139 | doc.editorState.snap.mode = snapMode; 140 | }, [snapMode]); 141 | 142 | // レイヤーの変更の対応 143 | useEffect(() => { 144 | doc.layers = layers; 145 | }, [layers]); 146 | 147 | // ウインドウのサイズ変更に伴うキャンバスのリサイズ 148 | useEffect(() => { 149 | const info = doc.canvasInfo; 150 | info.width = canvasWidth; 151 | info.height = canvasHeight; 152 | 153 | const bbox = ComputeBoundingBox.geometryObjects(doc.geometryObjects, false); 154 | const scaler = new CanvasScaler(info.height, info.width); 155 | info.scale = scaler.scaleFromBoundingBox(bbox); 156 | }, [canvasHeight, canvasWidth]); 157 | 158 | useEffect(() => { 159 | doc.canvasInfo.isActive = active; 160 | }, [active]); 161 | 162 | // 描画カラーの変更 163 | useEffect(() => { 164 | doc.canvasInfo.colorMode = theme.palette.mode; 165 | }, [theme.palette.mode]); 166 | 167 | // 階の選択による表示レイヤーの変更 168 | useEffect(() => { 169 | doc.canvasInfo.activeLayer = floor; 170 | doc.geometryObjects.forEach(obj => obj.isSelected = false); 171 | }, [floor]); 172 | 173 | // オリエンテーションの更新 174 | useEffect(() => { 175 | doc.canvasObject.orientation.northAngle = (northAxisError) ? 0 : northAxis; 176 | }, [northAxis, northAxisError]); 177 | 178 | return ( 179 | 187 | 188 | 189 | ); 190 | } 191 | -------------------------------------------------------------------------------- /editor-ui/EditPlansContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useMemo } from 'react'; 2 | import { CanvasDocument, CanvasInfo, Layer } from '../src/Canvas'; 3 | import { CanvasObject } from '../src/Canvas/Object'; 4 | import { SnapMode } from '../src/types'; 5 | 6 | interface EditPlansState { 7 | floor: string; 8 | setFloor: (floor: string) => void; 9 | northAxis: number; 10 | northAxisError: string; 11 | setNorthAxis: (northAxis: number) => void; 12 | editFunction: string; 13 | setEditFunction: (editFunction: string) => void; 14 | snapMode: SnapMode; 15 | setSnapMode: (snapMode: SnapMode) => void; 16 | layers: Layer[]; 17 | setLayers: (layers: Layer[]) => void; 18 | document: CanvasDocument; 19 | setDocument: (document: CanvasDocument) => void; 20 | active: boolean; 21 | setActive: (editorActive: boolean) => void; 22 | } 23 | 24 | const initialState: EditPlansState = { 25 | floor: "Layer0", 26 | setFloor: () => { }, 27 | northAxis: 0, 28 | northAxisError: '', 29 | setNorthAxis: () => { }, 30 | editFunction: 'AddRectangle', 31 | setEditFunction: () => { }, 32 | snapMode: { endPoint: true, near: false, middle: false, angle: false, grid: false }, 33 | setSnapMode: () => { }, 34 | layers: [], 35 | setLayers: () => { }, 36 | document: new CanvasDocument(new CanvasInfo(0, 0, 0), new CanvasObject(), [], []), 37 | setDocument: () => { }, 38 | active: true, 39 | setActive: () => { }, 40 | }; 41 | 42 | export const EditPlansContext = React.createContext(initialState); 43 | 44 | interface EditPlansProviderProps { 45 | children: React.ReactNode; 46 | } 47 | 48 | export function EditPlansProvider({ children }: EditPlansProviderProps): React.ReactElement { 49 | const [floor, setFloor] = useState(initialState.floor); 50 | const [northAxis, setNorthAxis] = useState(initialState.northAxis); 51 | const [editFunction, setEditFunction] = useState(initialState.editFunction); 52 | const [snapMode, setSnapMode] = useState(initialState.snapMode); 53 | const [layers, setLayers] = useState(initialState.layers); 54 | const [document, setDocument] = useState(initialState.document); 55 | const [active, setActive] = useState(initialState.active); 56 | const northAxisError = northAxis < -180 || northAxis > 180 ? 'NorthAxis must be from -180° to 180°' : ''; 57 | 58 | const state: EditPlansState = useMemo(() => { 59 | return { 60 | floor, 61 | setFloor, 62 | northAxis, 63 | northAxisError, 64 | setNorthAxis, 65 | editFunction, 66 | setEditFunction, 67 | snapMode, 68 | setSnapMode, 69 | layers, 70 | setLayers, 71 | document, 72 | setDocument, 73 | active, 74 | setActive, 75 | }; 76 | }, [ 77 | floor, 78 | northAxis, northAxisError, setNorthAxis, 79 | editFunction, setEditFunction, 80 | snapMode, setSnapMode, 81 | layers, setLayers, 82 | document, setDocument, 83 | active, setActive, 84 | ]); 85 | 86 | return {children}; 87 | } 88 | 89 | export function useEditPlansContext(): EditPlansState { 90 | return useContext(EditPlansContext); 91 | } 92 | -------------------------------------------------------------------------------- /editor-ui/InputSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Stack from '@mui/material/Stack'; 3 | import TextField from '@mui/material/TextField'; 4 | import FormControlLabel from '@mui/material/FormControlLabel'; 5 | import FormControl from '@mui/material/FormControl'; 6 | import FormLabel from '@mui/material/FormLabel'; 7 | import FormGroup from '@mui/material/FormGroup'; 8 | import Checkbox from '@mui/material/Checkbox'; 9 | import { useEditPlansContext } from './EditPlansContext'; 10 | import { Autocomplete, IconButton, Tooltip } from '@mui/material'; 11 | import { BorderColor, FitScreen, TransitEnterexit } from '@mui/icons-material'; 12 | import Box from '@mui/material/Box'; 13 | import dynamic from 'next/dynamic'; 14 | import { editorFunction } from '../src/EditorFunction'; 15 | 16 | const Sidebar = dynamic(() => import('./Sidebar'), { 17 | ssr: false, 18 | }); 19 | 20 | type SnapModeControlProp = { 21 | name: string; 22 | label: string; 23 | checked: boolean; 24 | }; 25 | 26 | function NorthAxisInput(): JSX.Element { 27 | const { northAxis, northAxisError, setNorthAxis } = useEditPlansContext(); 28 | 29 | return setNorthAxis(Number(e.target.value))} />; 40 | } 41 | 42 | function SnapModeControl(prop: SnapModeControlProp): JSX.Element { 43 | const { snapMode, setSnapMode } = useEditPlansContext(); 44 | 45 | return ( 46 | setSnapMode({ ...snapMode, [e.target.name]: e.target.checked })} />} 53 | /> 54 | ); 55 | } 56 | 57 | function SnapModeCheckBoxes(): JSX.Element { 58 | const { snapMode } = useEditPlansContext(); 59 | const { endPoint, near, middle, grid, angle } = snapMode; 60 | 61 | return 62 | SnapMode 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ; 73 | } 74 | 75 | function EditFunctionButtons(): JSX.Element { 76 | const { editFunction, setEditFunction } = useEditPlansContext(); 77 | 78 | function clickEditFunctionIcon(command: string): void { 79 | setEditFunction(command); 80 | (document.getElementById('functionCLI') as HTMLInputElement).value = command; 81 | } 82 | 83 | return ( 84 | 85 | 86 | clickEditFunctionIcon('Select')}> 87 | {editFunction === "Select" ? : } 88 | 89 | 90 | 91 | clickEditFunctionIcon('ZoomExtendAll')}> 92 | {editFunction === "ZoomExtendAll" ? : } 93 | 94 | 95 | 96 | clickEditFunctionIcon('AddRectangle')}> 97 | {editFunction === "AddRectangle" ? : } 98 | 99 | 100 | 101 | ); 102 | } 103 | 104 | function EditFunctionCLI(): JSX.Element { 105 | const { setEditFunction, setActive } = useEditPlansContext(); 106 | 107 | function changeCommand(command: string): void { 108 | const isCommandExist = editorFunction[command] === undefined ? false : true; 109 | if (isCommandExist) { 110 | setEditFunction(command); 111 | } else if (command === "" || command === null) { 112 | } else { 113 | alert('command-not-found' + command); 114 | } 115 | } 116 | 117 | const commandList: string[] = Object.keys(editorFunction).filter((key) => editorFunction[key].isCommand === true); 118 | 119 | return ( 120 | 121 | setActive(false)} 130 | onBlur={() => setActive(true)} 131 | onChange={(_event, value) => changeCommand(value!)} 132 | renderInput={(params) => } 138 | /> 139 | 140 | ); 141 | } 142 | 143 | export default function InputSidebar(): React.ReactElement { 144 | 145 | return ( 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /editor-ui/LayerContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | import { LayerItem } from './LayerItem'; 3 | import update from 'immutability-helper'; 4 | import { useEditPlansContext } from './EditPlansContext'; 5 | import { Button } from '@mui/material'; 6 | import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; 7 | import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'; 8 | import { Layer } from "../src/Canvas"; 9 | 10 | // FIXME: これまでの最大のレイヤーインデックスを保存している。もっと良い書き方がありそう 11 | let layerIndex = 0; 12 | 13 | export function LayerContainer(): React.ReactElement { 14 | const { floor, layers, setLayers } = useEditPlansContext(); 15 | const [layerItems, setLayerItems] = useState(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 |
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 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {title} 53 | 54 | 55 | 56 |
{children}
57 |
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 | 3 | 4 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------