트리 이미지 생성, 세 번째 부분.분형에서 진짜 나무로

우리 트리 이미지 생성기를 완성합시다!
두 번째 부분에서 우리는 응용 프로그램 구조를 설계하고 환경과 의존 주입을 설정했다.마지막으로 트리의 문자열 표현을 생성할 수 있는 L-Systems 모듈을 만들었습니다.
에서 캔버스 요소에 액세스하기 위해 형상 모듈과 DOM 어댑터를 생성했습니다.마지막으로, 우리는 화면에 첫 번째 그림을 보여 주었다.
마지막 글에서 우리는 L 시스템 역할에 대한 번역기를 만들 것이다.그 밖에 우리는 피타고라스 나무를 생성하고 랜덤성을 첨가하여 진정한 나무처럼 보일 것이다.
마지막으로 다음과 같이 트리 이미지를 생성하는 응용 프로그램을 만듭니다.

제작 번역


해석기 모듈은 L 시스템의 문자열 표현을 받아들여 하나의 그림 명령으로 번역합니다.API를 설계하고 종속성을 정의합니다.

이 모듈은 기하학적 모듈에 의존하고 SystemInterpreter 인터페이스를 제공한다.
// src/interpreter/types.ts

export interface SystemInterpreter {
  translate(expression: Expression): List<Line>;
}
이 인터페이스를 실현해 봅시다.
// src/interpreter/implementation.ts

import { AppSettings } from "../settings";
import { StartSelector } from "../geometry/location";
import { ShapeBuilder } from "../geometry/shape";
import { Stack } from "./stack/types";
import { SystemInterpreter } from "./types";

export class SystemToGeometryMapper implements SystemInterpreter {
  // We will change these fields while translating commands:
  private currentLocation: Point = { x: 0, y: 0 };
  private currentAngle: DegreesAmount = 0;

  // These fields keep characters of an initial expression
  // and a list of corresponding commands:
  private systemTokens: List<Character> = [];
  private drawInstructions: List<Line> = [];

  // Define dependencies:
  constructor(
    private shapeBuilder: ShapeBuilder,
    private startSelector: StartSelector,
    private stack: Stack<TreeJoint>,
    private settings: AppSettings,
  ) {}

  // Implement public methods:
  public translate(expression: Expression): List<Line> {
    this.currentLocation = { ...this.startSelector.selectStart() };
    this.systemTokens = expression.split("");
    this.systemTokens.forEach(this.translateToken);
    return this.drawInstructions;
  }

  // …
}
translate 방법에서는 L 시스템 표현식을 사용하여 단일 문자로 분할했습니다.그리고 우리는 translateToken 방법으로 모든 문자를 처리하고, 잠시 후에 이 방법을 작성할 것이다.
따라서 번역된 명령이 모두 포함된 drawInstructions 목록을 되돌려줍니다.

You may notice the Stack<TreeJoint> in the dependencies. This is literal stack structure implementation. You can find its code on GitHub.


그리고 privatetranslateToken 방법을 만듭니다.
// src/interpreter/implementation.ts

export class SystemToGeometryMapper implements SystemInterpreter {
  // …

  private translateToken = (token: Character): void => {
    switch (token) {
      // If the character is 0 or 1
      // we draw a line from the current position
      // with a current angle:
      case "0":
      case "1": {
        const line = this.shapeBuilder.createLine(
          this.currentLocation,
          this.settings.stemLength,
          this.currentAngle,
        );

        this.drawInstructions.push(line);
        this.currentLocation = { ...line.end };
        break;
      }

      // If the character is an opening bracket we turn left
      // and push the current position and angle in the stack:
      case "[": {
        this.currentAngle -= this.settings.jointAngle;
        this.stack.push({
          location: { ...this.currentLocation },
          rotation: this.currentAngle,
          stemWidth: this.settings.stemLength,
        });

        break;
      }

      // If the character is the closing bracket 
      // we pop the last position and the angle from the stack
      // and turn right from there:
      case "]": {
        const lastJoint = this.stack.pop();
        this.currentLocation = { ...lastJoint.location };
        this.currentAngle = lastJoint.rotation + 2 * this.settings.jointAngle;
        break;
      }
    }
  };
}
이제 영패에 이 방법을 사용할 때, 그것은 새로운 선을 그리거나, 어느 쪽으로 돌아갈지 결정한다.창고는 우리가 마지막 지점으로 돌아가는 것을 돕는다.
이제 설정을 업데이트하고 이 메서드를 실행하여 다음 사항을 살펴보겠습니다.
// src/settings/index.ts

export const settings: AppSettings = {
  canvasSize: {
    width: 800,
    height: 600,
  },

  // Using 5 iterations
  // with Pythagoras tree rules:
  iterations: 5,
  initiator: "0",
  rules: {
    "1": "11",
    "0": "1[0]0",
  },

  // Stem length is 10 pixels;
  // turn 45 degrees each time:
  stemLength: 10,
  jointAngle: 45,
};
애플리케이션 포털 업데이트:
// src/index.ts

const builder = container.get<SystemBuilder>();
const drawer = container.get<Drawer>();
const interpreter = container.get<SystemInterpreter>();
const settings = container.get<AppSettings>();

const system = builder.build(settings);
const lines = interpreter.translate(system);
lines.forEach((line) => drawer.drawLine(line));
이러한 설정을 통해 우리는 규범화된 피타고라스 나무를 얻었다.

우리는 각도 게임을 해서 우리가 어떤 데이터를 얻을 수 있는지 볼 수 있다😃
90도에서 우리는 안테나를 얻었다.

15도의 온도에서 우리는 풀 한 조각을 얻었다.

화씨 115도, 우리는 얻는다...음...

쿨!우리는 이미 이 나무의 기본 지식을 가지고 있다.하지만 우리가 그것을 더욱 진실하게 만들기 전에 입구점을 정리해야 한다.

입구점 정리


지금 입구가 좀 더러워요.
// src/index.ts

const builder = container.get<SystemBuilder>();
const drawer = container.get<Drawer>();
const interpreter = container.get<SystemInterpreter>();
const settings = container.get<AppSettings>();

const system = builder.build(settings);
const lines = interpreter.translate(system);
lines.forEach((line) => drawer.drawLine(line));
우리는 용기에서 너무 많은 서비스를 받아서 모든 조작을 수동으로 초기화했다.응용 프로그램 시작을 담당하는 개별 객체에 이러한 모든 것을 숨깁니다.
// src/app/types.ts

export interface Application {
  start(): void;
}
이제 모든 코드를 start 메서드 뒤에 숨깁니다.
// src/app/implementation.ts

export class App implements Application {
  constructor(
    private builder: SystemBuilder,
    private drawer: Drawer,
    private interpreter: SystemInterpreter,
    private settings: AppSettings,
  ) {}

  start(): void {
    const system = this.builder.build(this.settings);
    const lines = this.interpreter.translate(system);
    lines.forEach((line) => this.drawer.drawLine(line));
  }
}
...등록:
// src/app/composition.ts

import { container } from "../composition";
import { App } from "./implementation";
import { Application } from "./types";

container.registerSingleton<Application, App>();
이제 입구가 더 깨끗해졌어요.
// src/index.ts

import { container } from "./composition";
import { Application } from "./app/types";

const app = container.get<Application>();
app.start();

나무를 더 진실하게


지금 우리의 나무는 너무 엄격하고 수학적이다.보다 사실적으로 보이기 위해서는 다음과 같은 몇 가지 임의성과 역동성을 추가해야 합니다.
  • 나무가 성장함에 따라 줄기의 넓이는 줄여야 한다.
  • 각도는 랜덤으로 표준치에서 벗어나야 한다.
  • 분지는 상대적으로 무작위적인 곳에서 나타나야 한다.
  • 잎은 녹색이어야 한다😃
  • 우선, 우리는 L 시스템의 규칙을 약간 바꾸었다.우리는 새로운 상수 "2" 를 추가했다.현재 나무 모양의 분지는 매번 교체될 때마다 두 배로 줄어들고 새로운 상수는 이 과정을 늦춘다.
    우리는 또 공리를 좀 더 길게 해서 나무 줄기를 더욱 길게 할 것이다.마지막으로 우리는 교체 횟수를 12회로 늘릴 것이다.
    // src/settings/index.ts
    
    export const settings: AppSettings = {
      // …
    
      iterations: 12,
      initiator: "22220",
      rules: {
        "1": "21",
        "0": "1[20]20",
      },
    
      leafWidth: 4,
      stemWidth: 16,
    
      // …
    };
    
    이제 해석기 코드를 변경하겠습니다.
    export class SystemToGeometryMapper implements SystemInterpreter {
      private currentLocation: Point = { x: 0, y: 0 };
      private currentAngle: DegreesAmount = 0;
    
      // We will also change the stem width:
      private currentWidth: PixelsAmount = 0;
    
      private systemTokens: List<Character> = [];
      private drawInstructions: List<Instruction> = [];
    
      constructor(
        private shapeBuilder: ShapeBuilder,
        private startSelector: StartSelector,
        private stack: Stack<TreeJoint>,
        private settings: AppSettings,
    
        // Here, we're going to need a random source.
        // In our case, it is a wrapper over `Math.random`
        // with a bit more convenient API.
        // You can find its source on GitHub as well.
        private random: RandomSource,
      ) {}
    
      // …
    }
    
    그리고 잎사귀 (("0" 문자) 를 처리하면 녹색을 무작위로 선택합니다.
    private translateToken = (token: Character): void => {
      switch (token) {
        case "0": {
          const line = this.createLine();
    
          this.currentLocation = { ...line.end };
          this.drawInstructions.push({
            line,
            color: this.selectLeafColor(),  // Adding the leaf color
            width: this.settings.leafWidth, // and width.
          });
    
          break;
        }
    
        // …
      }
    }
    
    그리고 나서 우리는 때때로 새로운 지점을 뛰어넘는다.이로 인해 브랜치 위치가 더욱 혼란스러워집니다.
    private translateToken = (token: Character): void => {
      switch (token) {
        // …
    
        case "1":
        case "2": {
          // Draw a new branch only in 60% of cases:
          if (this.shouldSkip()) return;
    
          const line = this.createLine();
          this.drawInstructions.push({ line, width: this.currentWidth });
          this.currentLocation = { ...line.end };
    
          break;
        }
    
        // …
      }
    };
    
    회전 시 각도 값에 임의 편차를 추가합니다.
    private translateToken = (token: Character): void => {
      switch (token) {
        // …
    
        case "[": {
          // Making the width smaller:
          this.currentWidth *= 0.75;
    
          // Adding a random angle deviation:
          this.currentAngle -=
            this.settings.jointAngle + this.randomAngleDeviation();
    
          // Remember the branching position,
          // angle, and current branch width:
          this.stack.push({
            location: { ...this.currentLocation },
            rotation: this.currentAngle,
            stemWidth: this.currentWidth,
          });
    
          break;
        }
    
        case "]": {
          // Getting the last branching position:
          const lastJoint = this.stack.pop();
    
          // Using its position, angle, and width as current:
          this.currentWidth = lastJoint.stemWidth;
          this.currentLocation = { ...lastJoint.location };
          this.currentAngle =
            lastJoint.rotation +
            2 * this.settings.jointAngle +
            this.randomAngleDeviation();
    
          break;
        }
      }
    };
    
    또한 부족한 모든 사유 방법도 추가했습니다.
    export class SystemToGeometryMapper implements SystemInterpreter {
      // …
    
      private createLine = (): Line => {
        return this.shapeBuilder.createLine(
          this.currentLocation,
          this.settings.stemLength,
          this.currentAngle,
        );
      };
    
      // Draw branches only 60% of the time:
      private shouldSkip = (): boolean => {
        return this.random.getValue() > 0.4;
      };
    
      // Random deviation will be from -5 to 5 degrees:
      private randomAngleDeviation = (): Angle => {
        return this.random.getBetweenInclusive(-5, 5);
      };
    
      // Green color will be chosen among 3 different colors:
      private selectLeafColor = (): Color => {
        const randomColor = this.random.getBetweenInclusive(0, 2);
        return leafColors[randomColor];
      };
    }
    
    마지막으로 애플리케이션을 실행하고 결과를 살펴보겠습니다.

    우리는 진정한 나무 한 그루가 있다!🌳

    변화는 현지의 것이다


    중요한 것은 최근에 변경한 사항이 Interpreter 모듈의 제한을 받았다는 것이다.비록 이미지에 커다란 변화가 생겼지만, 우리는 해석기의 실현만 바꾸었다.다른 모든 모듈은 그대로 유지됩니다.
    더 중요한 것은 인터페이스도 마찬가지다.우리는 SystemInterpreterShapeBuilder를 바꿀 필요가 없다.

    우리는 심지어 완전히 바꾸어 실현할 수 있다!인터페이스가 같기만 하면 프로그램은 아무런 추가 변경도 필요 없이 작업을 할 수 있다.

    결실


    이제 전체 시스템을 살펴보겠습니다.

    모듈은 인터페이스를 통해 통신한다.
    그것은 테스트에 편리하다.각 모듈은 개별적으로 테스트할 수 있습니다.의존항은 mock objects로 바꾸어 같은 인터페이스를 실현할 수 있다.
    인터페이스는 세부 사항을 테스트할 필요가 없기 때문에 우리에게 무엇을 테스트해야 하는지 알려준다.인터페이스는 공용 API를 보여주고 테스트할 내용을 보여줍니다.
    또 다른 장점은 서로 다른 가방에서 모듈을 조합하는 것이다.“DDD, Hexagonal, Onion, Clean, CQRS, …How I put it all together”에서 이에 대해 상세하게 설명하였다.
    예를 들어, 응용 프로그램 계층별로 패키지를 분할할 수 있습니다.

    의존항은 항상 영역을 가리킨다.이것은 역층을 완전히 독립시키기 때문에 우리는 많은 다른 응용 프로그램 사이에서 코드를 공유할 수 있다.

    내부 핵과 인프라 공유에 대한 방주


    인프라는 일반적으로 데이터베이스, 검색엔진 및 기타 외부 구동 서비스를 연결하는 코드이다.
    Google 응용 프로그램에는 결과를 어떤 방식으로도 저장할 필요가 없기 때문에 인프라가 없습니다.
    만약 우리가 그것을 가지고 있다면 인프라 시설 모듈은 매우 비슷할 것이다.
  • 응용층은 이미지를 저장하는 조건을 설명한다.
  • 응용 층에는 일부 포트가 포함될 것이다. 이 포트들은 우리 응용 프로그램이 결과를 어떻게 저장하길 원하는지 설명할 것이다.
  • 어댑터는 외부 인터페이스를 우리 프로그램과 호환시킬 것입니다.
  • 좋은 웹페이지 즐겨찾기